diff --git a/src/auth/src/EloquentUserProvider.php b/src/auth/src/EloquentUserProvider.php
index 5340d478c..c59730789 100755
--- a/src/auth/src/EloquentUserProvider.php
+++ b/src/auth/src/EloquentUserProvider.php
@@ -545,7 +545,8 @@ protected function registerCacheInvalidationEvents(): void
$modelClass = $this->model;
// Insert or replace the descriptor — duplicate configs collapse.
- $descriptorKey = md5(
+ $descriptorKey = hash(
+ 'xxh128',
($this->cacheStoreName ?? '') . '|' . $this->cachePrefix . '|' . $this->modelSegment
);
diff --git a/src/auth/src/SessionGuard.php b/src/auth/src/SessionGuard.php
index 447e4cc53..b5fb097b5 100644
--- a/src/auth/src/SessionGuard.php
+++ b/src/auth/src/SessionGuard.php
@@ -99,7 +99,7 @@ public function __construct(
$this->provider = $provider;
$this->timebox = $timebox ?: new Timebox;
- $classHash = sha1(static::class);
+ $classHash = hash('xxh128', static::class);
$this->hashedName = 'login_' . $this->name . '_' . $classHash;
$this->hashedRecallerName = 'remember_' . $this->name . '_' . $classHash;
}
diff --git a/src/auth/src/TokenGuard.php b/src/auth/src/TokenGuard.php
index 16e3e9efa..86aa62ba1 100644
--- a/src/auth/src/TokenGuard.php
+++ b/src/auth/src/TokenGuard.php
@@ -157,7 +157,7 @@ protected function getContextKeyForToken(?string $token): string
return "__auth.guards.{$this->name}.user.default";
}
- return "__auth.guards.{$this->name}.user." . md5($token);
+ return "__auth.guards.{$this->name}.user." . hash('xxh128', $token);
}
/**
diff --git a/src/boost/docs/deployment.md b/src/boost/docs/deployment.md
index 54a1930fc..78f32ccad 100644
--- a/src/boost/docs/deployment.md
+++ b/src/boost/docs/deployment.md
@@ -126,7 +126,7 @@ In production, your Hypervel server should be kept running by a process monitor,
php artisan serve
```
-By default, the HTTP server binds to `0.0.0.0:9501`. You may configure the server host, port, worker count, and other Swoole settings using the `HTTP_SERVER_HOST`, `HTTP_SERVER_PORT`, and `SERVER_WORKERS_NUMBER` environment variables read by `config/server.php`.
+By default, the HTTP server binds to `0.0.0.0:9501` with HTTP/2 enabled. You may configure the server host, port, worker count, max requests per worker, HTTP/2 support, and other Swoole settings using the `HTTP_SERVER_HOST`, `HTTP_SERVER_PORT`, `SERVER_WORKERS`, `SERVER_MAX_REQUESTS`, and `SERVER_HTTP2` environment variables read by `config/server.php`.
### Directory Permissions
diff --git a/src/boost/docs/reverb.md b/src/boost/docs/reverb.md
index fac6dd7a6..188b15217 100644
--- a/src/boost/docs/reverb.md
+++ b/src/boost/docs/reverb.md
@@ -420,7 +420,7 @@ hypervel hard nofile 10000
### Workers
-Reverb runs on Swoole's native WebSocket server and shares the configured WebSocket port across your Swoole workers. Increasing the number of workers increases the amount of parallel work Reverb can perform, but each worker also uses its own memory. You may configure the worker count using the `SERVER_WORKERS_NUMBER` environment variable read by your application's `config/server.php` configuration file.
+Reverb runs on Swoole's native WebSocket server and shares the configured WebSocket port across your Swoole workers. Increasing the number of workers increases the amount of parallel work Reverb can perform, but each worker also uses its own memory. You may configure the worker count using the `SERVER_WORKERS` environment variable read by your application's `config/server.php` configuration file.
In single-instance mode, Reverb uses a Swoole table to track channel subscription counts, presence member counts, webhook locks, and per-application connection limits across workers. The default table sizes are suitable for most applications, but you may tune them if you have a large number of channels or presence members:
diff --git a/src/cache/src/FailoverStore.php b/src/cache/src/FailoverStore.php
index c01a2dd10..bbb03d692 100644
--- a/src/cache/src/FailoverStore.php
+++ b/src/cache/src/FailoverStore.php
@@ -212,7 +212,7 @@ protected function attemptOnAllStores(string $method, array $arguments): mixed
$failedCaches[] = $store;
- if (! in_array($store, $failingCaches)) {
+ if (! in_array($store, $failingCaches) && $this->events->hasListeners(CacheFailedOver::class)) {
$this->events->dispatch(new CacheFailedOver($store, $e));
}
}
diff --git a/src/cache/src/FileStore.php b/src/cache/src/FileStore.php
index 2c0020dcf..1c4f83a79 100644
--- a/src/cache/src/FileStore.php
+++ b/src/cache/src/FileStore.php
@@ -399,7 +399,7 @@ protected function emptyPayload(): array
*/
public function path(string $key): string
{
- $parts = array_slice(str_split($hash = sha1($key), 2), 0, 2);
+ $parts = array_slice(str_split($hash = hash('xxh128', $key), 2), 0, 2);
return $this->directory . '/' . implode('/', $parts) . '/' . $hash;
}
diff --git a/src/cache/src/Redis/AllTaggedCache.php b/src/cache/src/Redis/AllTaggedCache.php
index 09cb6abf5..1ada608f8 100644
--- a/src/cache/src/Redis/AllTaggedCache.php
+++ b/src/cache/src/Redis/AllTaggedCache.php
@@ -130,7 +130,7 @@ public function putMany(array $values, DateInterval|DateTimeInterface|int|null $
$values,
$seconds,
$this->tags->tagIds(),
- sha1($this->tags->getNamespace()) . ':'
+ hash('xxh128', $this->tags->getNamespace()) . ':'
);
if ($result) {
diff --git a/src/cache/src/Redis/Console/Benchmark/BenchmarkContext.php b/src/cache/src/Redis/Console/Benchmark/BenchmarkContext.php
index dd297ffdb..196d806a4 100644
--- a/src/cache/src/Redis/Console/Benchmark/BenchmarkContext.php
+++ b/src/cache/src/Redis/Console/Benchmark/BenchmarkContext.php
@@ -148,7 +148,7 @@ public function getTagStoragePatterns(string $tagNamePrefix): array
* Returns patterns for BOTH tag modes to ensure complete cleanup
* regardless of current mode (important for --compare-tag-modes):
* - Untagged keys: {cachePrefix}{keyPrefix}* (same in both modes)
- * - Tagged keys in all mode: {cachePrefix}{sha1}:{keyPrefix}* (namespaced)
+ * - Tagged keys in all mode: {cachePrefix}{xxh128}:{keyPrefix}* (namespaced)
*
* @param string $keyPrefix The prefix to match cache keys against
* @return array Patterns to use with SCAN/KEYS commands
@@ -160,7 +160,7 @@ public function getCacheValuePatterns(string $keyPrefix): array
return [
// Untagged cache values (both modes) and any-mode tagged values
$prefix . $keyPrefix . '*',
- // All-mode tagged values at {cachePrefix}{sha1}:{keyName}
+ // All-mode tagged values at {cachePrefix}{xxh128}:{keyName}
$prefix . '*:' . $keyPrefix . '*',
];
}
@@ -268,7 +268,7 @@ public function cleanup(): void
$store->tags($tags)->flush();
// 2. Clean up non-tagged benchmark keys using mode-aware patterns
- // In all mode, tagged keys are at {prefix}{sha1}:{key}, so we need multiple patterns
+ // In all mode, tagged keys are at {prefix}{xxh128}:{key}, so we need multiple patterns
foreach ($this->getCacheValuePatterns(self::KEY_PREFIX) as $pattern) {
$this->flushKeysByPattern($storeInstance, $pattern);
}
diff --git a/src/cache/src/Redis/Console/Doctor/Checks/ForeverStorageCheck.php b/src/cache/src/Redis/Console/Doctor/Checks/ForeverStorageCheck.php
index bcee7f7b8..24972c35b 100644
--- a/src/cache/src/Redis/Console/Doctor/Checks/ForeverStorageCheck.php
+++ b/src/cache/src/Redis/Console/Doctor/Checks/ForeverStorageCheck.php
@@ -46,7 +46,7 @@ public function run(DoctorContext $context): CheckResult
);
$this->testAnyModeHashTtl($context, $result, $foreverTag, $foreverKey);
} else {
- // All mode: key is namespaced with sha1 of tag IDs
+ // All mode: key is namespaced with xxh128 of tag IDs
$namespacedKey = $context->namespacedKey([$foreverTag], $foreverKey);
$keyTtl = $context->redis->ttl($context->cachePrefix . $namespacedKey);
$result->assert(
diff --git a/src/cache/src/Redis/Console/Doctor/Checks/TaggedOperationsCheck.php b/src/cache/src/Redis/Console/Doctor/Checks/TaggedOperationsCheck.php
index 052b48b02..017a56508 100644
--- a/src/cache/src/Redis/Console/Doctor/Checks/TaggedOperationsCheck.php
+++ b/src/cache/src/Redis/Console/Doctor/Checks/TaggedOperationsCheck.php
@@ -40,7 +40,7 @@ public function run(DoctorContext $context): CheckResult
);
$this->testAnyMode($context, $result, $tag, $key);
} else {
- // All mode: key is namespaced with sha1 of tags
+ // All mode: key is namespaced with xxh128 of tags
// Direct get without tags will NOT find the item
$result->assert(
$context->cache->get($key) === null,
diff --git a/src/cache/src/Redis/Console/Doctor/DoctorContext.php b/src/cache/src/Redis/Console/Doctor/DoctorContext.php
index 4fde85f3b..71b4bc289 100644
--- a/src/cache/src/Redis/Console/Doctor/DoctorContext.php
+++ b/src/cache/src/Redis/Console/Doctor/DoctorContext.php
@@ -55,22 +55,11 @@ public function tagHashKey(string $tag): string
return $this->store->getContext()->tagHashKey($tag);
}
- /**
- * Get the tag identifier (without cache prefix).
- *
- * Format: "_any:tag:{tagName}:entries" or "_all:tag:{tagName}:entries"
- * Used for namespace computation in all mode.
- */
- public function tagId(string $tag): string
- {
- return $this->store->getContext()->tagId($tag);
- }
-
/**
* Compute the namespaced key for a tagged cache item in all mode.
*
- * In all mode, cache keys are prefixed with sha1 of sorted tag IDs.
- * Format: "{sha1}:{key}"
+ * In all mode, cache keys are prefixed with xxh128 of tag IDs in the requested order.
+ * Format: "{xxh128}:{key}"
*
* @param array $tags The tag names
* @param string $key The cache key
@@ -78,9 +67,7 @@ public function tagId(string $tag): string
*/
public function namespacedKey(array $tags, string $key): string
{
- $tagIds = array_map(fn (string $tag) => $this->tagId($tag), $tags);
- sort($tagIds);
- $namespace = sha1(implode('|', $tagIds));
+ $namespace = hash('xxh128', $this->cache->tags($tags)->getTags()->getNamespace());
return $namespace . ':' . $key;
}
@@ -156,7 +143,7 @@ public function getTagStoragePatterns(string $tagNamePrefix): array
* Returns patterns for BOTH tag modes to ensure complete cleanup
* regardless of current mode (e.g., if config changed between runs):
* - Untagged keys: {cachePrefix}{keyPrefix}* (same in both modes)
- * - Tagged keys in all mode: {cachePrefix}{sha1}:{keyPrefix}* (namespaced)
+ * - Tagged keys in all mode: {cachePrefix}{xxh128}:{keyPrefix}* (namespaced)
*
* @param string $keyPrefix The prefix to match cache keys against
* @return array Patterns to use with SCAN/KEYS commands
@@ -166,7 +153,7 @@ public function getCacheValuePatterns(string $keyPrefix): array
return [
// Untagged cache values (both modes) and any-mode tagged values
$this->cachePrefix . $keyPrefix . '*',
- // All-mode tagged values at {cachePrefix}{sha1}:{keyName}
+ // All-mode tagged values at {cachePrefix}{xxh128}:{keyName}
$this->cachePrefix . '*:' . $keyPrefix . '*',
];
}
diff --git a/src/cache/src/Redis/Console/DoctorCommand.php b/src/cache/src/Redis/Console/DoctorCommand.php
index c93cc39d7..89dd05615 100644
--- a/src/cache/src/Redis/Console/DoctorCommand.php
+++ b/src/cache/src/Redis/Console/DoctorCommand.php
@@ -109,42 +109,40 @@ public function handle(): int
$this->info('Checking System Requirements...');
$this->newLine();
- if (! $this->runEnvironmentChecks($storeName, $store, $tagMode)) {
- return self::FAILURE;
- }
+ return $store->getContext()->withConnection(function (RedisConnection $redis) use ($repository, $store, $storeName, $tagMode) {
+ if (! $this->runEnvironmentChecks($storeName, $store, $tagMode, $redis)) {
+ return self::FAILURE;
+ }
- $this->info('✓ All requirements met!');
- $this->newLine(2);
+ $this->info('✓ All requirements met!');
+ $this->newLine(2);
- $this->info("Testing cache store: {$storeName}> ({$tagMode} mode)");
- $this->newLine();
+ $this->info("Testing cache store: {$storeName}> ({$tagMode} mode)");
+ $this->newLine();
- // Get the Redis connection from the store's context
- $context = $store->getContext();
- $redis = $context->withConnection(fn (RedisConnection $connection) => $connection);
+ $doctorContext = new DoctorContext(
+ cache: $repository,
+ store: $store,
+ redis: $redis,
+ cachePrefix: $store->getPrefix(),
+ storeName: $storeName,
+ );
- $doctorContext = new DoctorContext(
- cache: $repository,
- store: $store,
- redis: $redis,
- cachePrefix: $store->getPrefix(),
- storeName: $storeName,
- );
-
- // Run functional checks with cleanup
- try {
- $this->cleanup($doctorContext, silent: true);
- $this->runFunctionalChecks($doctorContext);
- } finally {
- $this->cleanup($doctorContext);
- }
+ // Run functional checks with cleanup
+ try {
+ $this->cleanup($doctorContext, silent: true);
+ $this->runFunctionalChecks($doctorContext);
+ } finally {
+ $this->cleanup($doctorContext);
+ }
- // Run cleanup verification after cleanup
- $this->runCleanupVerification($doctorContext);
+ // Run cleanup verification after cleanup
+ $this->runCleanupVerification($doctorContext);
- $this->displaySummary();
+ $this->displaySummary();
- return $this->testsFailed === 0 ? self::SUCCESS : self::FAILURE;
+ return $this->testsFailed === 0 ? self::SUCCESS : self::FAILURE;
+ });
}
/**
@@ -152,12 +150,8 @@ public function handle(): int
*
* @return list
*/
- protected function getEnvironmentChecks(string $storeName, RedisStore $store, string $tagMode): array
+ protected function getEnvironmentChecks(string $storeName, RedisStore $store, string $tagMode, RedisConnection $redis): array
{
- // Get connection for version checks
- $context = $store->getContext();
- $redis = $context->withConnection(fn (RedisConnection $connection) => $connection);
-
return [
new PhpRedisCheck,
new RedisVersionCheck($redis, $tagMode),
@@ -197,11 +191,11 @@ protected function getFunctionalChecks(): array
/**
* Run environment checks. Returns false if any check fails.
*/
- protected function runEnvironmentChecks(string $storeName, RedisStore $store, string $taggingMode): bool
+ protected function runEnvironmentChecks(string $storeName, RedisStore $store, string $taggingMode, RedisConnection $redis): bool
{
$allPassed = true;
- foreach ($this->getEnvironmentChecks($storeName, $store, $taggingMode) as $check) {
+ foreach ($this->getEnvironmentChecks($storeName, $store, $taggingMode, $redis) as $check) {
$result = $check->run();
foreach ($result->assertions as $assertion) {
diff --git a/src/cache/src/Redis/Support/StoreContext.php b/src/cache/src/Redis/Support/StoreContext.php
index b59f9f0af..f582e1937 100644
--- a/src/cache/src/Redis/Support/StoreContext.php
+++ b/src/cache/src/Redis/Support/StoreContext.php
@@ -69,7 +69,7 @@ public function tagMode(): TagMode
/**
* Get the tag identifier (without cache prefix).
*
- * Used by All mode for namespace computation (sha1 of sorted tag IDs).
+ * Used by all mode for namespace computation (xxh128 of tag IDs in requested order).
* Format: "_any:tag:{tagName}:entries" or "_all:tag:{tagName}:entries"
*/
public function tagId(string $tag): string
diff --git a/src/cache/src/Repository.php b/src/cache/src/Repository.php
index 95f266b81..0b2b7b26e 100644
--- a/src/cache/src/Repository.php
+++ b/src/cache/src/Repository.php
@@ -1066,7 +1066,7 @@ public function getRaw(UnitEnum|string $key): mixed
* "genuinely absent = miss".
*
* Applies itemKey() to each key so tag-namespacing works correctly on
- * TaggedCache / AllTaggedCache (which prepend sha1($tagNamespace) . ':').
+ * TaggedCache / AllTaggedCache (which prepend hash('xxh128', $tagNamespace) . ':').
* AnyTaggedCache overrides this method to throw, preserving the any-mode
* invariant that reads through tags are rejected.
*
diff --git a/src/cache/src/TaggedCache.php b/src/cache/src/TaggedCache.php
index 2e7f6a5e4..c90f590b1 100644
--- a/src/cache/src/TaggedCache.php
+++ b/src/cache/src/TaggedCache.php
@@ -82,7 +82,7 @@ public function flush(): bool
*/
public function taggedItemKey(string $key): string
{
- return sha1($this->tags->getNamespace()) . ':' . $key;
+ return hash('xxh128', $this->tags->getNamespace()) . ':' . $key;
}
/**
diff --git a/src/console/src/Scheduling/CallbackEvent.php b/src/console/src/Scheduling/CallbackEvent.php
index aace89eb7..7e740a8ef 100644
--- a/src/console/src/Scheduling/CallbackEvent.php
+++ b/src/console/src/Scheduling/CallbackEvent.php
@@ -161,7 +161,7 @@ public function getSummaryForDisplay(): string
*/
public function mutexName(): string
{
- return 'framework/schedule-' . sha1($this->description ?? '');
+ return 'framework/schedule-' . hash('xxh128', $this->description ?? '');
}
/**
diff --git a/src/console/src/Scheduling/Event.php b/src/console/src/Scheduling/Event.php
index fb28b6c0b..10b7a6281 100644
--- a/src/console/src/Scheduling/Event.php
+++ b/src/console/src/Scheduling/Event.php
@@ -473,7 +473,7 @@ protected function ensureOutputIsBeingCaptured(): void
{
if (is_null($this->output)) {
$this->ensureOutputIsBeingCaptured = true;
- $this->sendOutputTo(storage_path('logs/schedule-' . sha1($this->mutexName()) . '.log'));
+ $this->sendOutputTo(storage_path('logs/schedule-' . hash('xxh128', $this->mutexName()) . '.log'));
}
}
@@ -764,7 +764,7 @@ public function mutexName(): string
}
return 'framework' . DIRECTORY_SEPARATOR . 'schedule-'
- . sha1($this->expression . static::normalizeCommand($this->command ?? ''));
+ . hash('xxh128', $this->expression . static::normalizeCommand($this->command ?? ''));
}
/**
diff --git a/src/database/src/DatabaseManager.php b/src/database/src/DatabaseManager.php
index 60004b3e0..18d238ffb 100755
--- a/src/database/src/DatabaseManager.php
+++ b/src/database/src/DatabaseManager.php
@@ -9,6 +9,7 @@
use DateTimeInterface;
use Hypervel\Container\Container;
use Hypervel\Context\CoroutineContext;
+use Hypervel\Contracts\Config\Repository as ConfigRepository;
use Hypervel\Contracts\Container\Container as ContainerContract;
use Hypervel\Contracts\Events\Dispatcher;
use Hypervel\Contracts\Foundation\Application;
@@ -17,7 +18,7 @@
use Hypervel\Database\Events\QueryExecuted;
use Hypervel\Database\Pool\PoolFactory;
use Hypervel\Support\Arr;
-use Hypervel\Support\Collection;
+use Hypervel\Support\Fluent;
use Hypervel\Support\InteractsWithTime;
use Hypervel\Support\Traits\Macroable;
use InvalidArgumentException;
@@ -54,13 +55,6 @@ class DatabaseManager implements ConnectionResolverInterface
*/
protected array $connections = [];
- /**
- * The dynamically configured (DB::build) connection configurations.
- *
- * @var array
- */
- protected array $dynamicConnectionConfigurations = [];
-
/**
* The callback to be executed to reconnect to a database.
*/
@@ -140,16 +134,6 @@ public function build(array $config): ConnectionInterface
);
}
- /**
- * Calculate the dynamic connection name for an on-demand connection based on its configuration.
- */
- public static function calculateDynamicConnectionName(array $config): string
- {
- return 'dynamic_' . md5((new Collection($config))->map(function ($value, $key) {
- return $key . (is_string($value) || is_int($value) ? $value : '');
- })->implode(''));
- }
-
/**
* Get a database connection instance from the given configuration.
*
@@ -180,9 +164,10 @@ protected function makeConnection(string $name): Connection
*/
protected function configuration(string $name): array
{
- $connections = $this->app['config']['database.connections'];
+ /** @var array $connections */
+ $connections = $this->configValue('database.connections', []);
- $config = $this->dynamicConnectionConfigurations[$name] ?? Arr::get($connections, $name);
+ $config = Arr::get($connections, $name);
if (is_null($config)) {
throw new InvalidArgumentException("Database connection [{$name}] not configured.");
@@ -373,8 +358,11 @@ protected function refreshPdoConnections(string $name): Connection
*/
public function getDefaultConnection(): ?string
{
+ /** @var null|string $defaultConnection */
+ $defaultConnection = $this->configValue('database.default');
+
return CoroutineContext::get(ConnectionResolver::DEFAULT_CONNECTION_CONTEXT_KEY)
- ?? $this->app['config']['database.default'];
+ ?? $defaultConnection;
}
/**
@@ -443,6 +431,17 @@ public function setDefaultConnection(?string $name): void
}
}
+ /**
+ * Get a config value.
+ */
+ protected function configValue(string $key, mixed $default = null): mixed
+ {
+ /** @var ConfigRepository|Fluent $config */
+ $config = $this->app->make('config');
+
+ return $config[$key] ?? $default;
+ }
+
/**
* Get the context key for storing a connection.
*
diff --git a/src/filesystem/src/Filesystem.php b/src/filesystem/src/Filesystem.php
index 67d509ca4..e93c6327a 100644
--- a/src/filesystem/src/Filesystem.php
+++ b/src/filesystem/src/Filesystem.php
@@ -159,8 +159,11 @@ public function lines(string $path): LazyCollection
/**
* Get the hash of the file at the given path.
+ *
+ * Defaults to xxh128 for fast app-internal file hashing. Pass a specific
+ * algorithm explicitly when an external protocol requires one.
*/
- public function hash(string $path, string $algorithm = 'md5'): string|false
+ public function hash(string $path, string $algorithm = 'xxh128'): string|false
{
return hash_file($algorithm, $path);
}
diff --git a/src/foundation/config/server.php b/src/foundation/config/server.php
index ff3ed1581..dffe1541b 100644
--- a/src/foundation/config/server.php
+++ b/src/foundation/config/server.php
@@ -48,12 +48,12 @@
'document_root' => base_path('public'),
'enable_static_handler' => (bool) env('SERVER_STATIC_FILE_HANDLER', true),
Constant::OPTION_ENABLE_COROUTINE => true,
- Constant::OPTION_WORKER_NUM => env('SERVER_WORKERS_NUMBER', swoole_cpu_num()),
+ Constant::OPTION_WORKER_NUM => env('SERVER_WORKERS', swoole_cpu_num()),
Constant::OPTION_PID_FILE => storage_path('framework/hypervel.pid'),
Constant::OPTION_OPEN_TCP_NODELAY => true,
Constant::OPTION_MAX_COROUTINE => 100000,
- Constant::OPTION_OPEN_HTTP2_PROTOCOL => true,
- Constant::OPTION_MAX_REQUEST => 100000,
+ Constant::OPTION_OPEN_HTTP2_PROTOCOL => (bool) env('SERVER_HTTP2', true),
+ Constant::OPTION_MAX_REQUEST => (int) env('SERVER_MAX_REQUESTS', 100000),
Constant::OPTION_MAX_WAIT_TIME => 3,
Constant::OPTION_SOCKET_BUFFER_SIZE => 2 * 1024 * 1024,
Constant::OPTION_BUFFER_OUTPUT_SIZE => 2 * 1024 * 1024,
diff --git a/src/foundation/src/Testing/Concerns/InteractsWithRedis.php b/src/foundation/src/Testing/Concerns/InteractsWithRedis.php
index ae18ee689..bf366d5bf 100644
--- a/src/foundation/src/Testing/Concerns/InteractsWithRedis.php
+++ b/src/foundation/src/Testing/Concerns/InteractsWithRedis.php
@@ -354,7 +354,7 @@ protected function cleanupRedisKeysWithPatterns(string ...$patterns): void
*/
protected function createRedisConnectionWithPrefix(string $optPrefix): string
{
- $connectionName = 'test_opt_' . ($optPrefix === '' ? 'none' : md5($optPrefix));
+ $connectionName = 'test_opt_' . ($optPrefix === '' ? 'none' : hash('xxh128', $optPrefix));
$config = $this->app->make('config');
diff --git a/src/foundation/src/Vite.php b/src/foundation/src/Vite.php
index 60035257f..53290ff66 100644
--- a/src/foundation/src/Vite.php
+++ b/src/foundation/src/Vite.php
@@ -828,7 +828,7 @@ public function manifestHash(?string $buildDirectory = null): ?string
return null;
}
- return md5_file($path) ?: null;
+ return hash_file('xxh128', $path) ?: null;
}
/**
diff --git a/src/horizon/src/Notifications/LongWaitDetected.php b/src/horizon/src/Notifications/LongWaitDetected.php
index 9280888ff..02dec6a37 100644
--- a/src/horizon/src/Notifications/LongWaitDetected.php
+++ b/src/horizon/src/Notifications/LongWaitDetected.php
@@ -108,6 +108,6 @@ public function toSlack(mixed $notifiable): ChannelIdSlackMessage|SlackMessage
*/
public function signature(): string
{
- return md5($this->longWaitConnection . $this->longWaitQueue);
+ return hash('xxh128', $this->longWaitConnection . $this->longWaitQueue);
}
}
diff --git a/src/http/src/Request.php b/src/http/src/Request.php
index 87f4de1db..5de5543f9 100644
--- a/src/http/src/Request.php
+++ b/src/http/src/Request.php
@@ -1092,7 +1092,7 @@ public function fingerprint(): string
throw new RuntimeException('Unable to generate fingerprint. Route unavailable.');
}
- return sha1(implode('|', array_merge(
+ return hash('xxh128', implode('|', array_merge(
$route->methods(),
[$route->getDomain(), $route->uri(), $this->ip()]
)));
diff --git a/src/jwt/src/JwtGuard.php b/src/jwt/src/JwtGuard.php
index 0360edc60..0f072d291 100644
--- a/src/jwt/src/JwtGuard.php
+++ b/src/jwt/src/JwtGuard.php
@@ -183,7 +183,7 @@ public function getPayload(): array
*/
protected function decodeToken(string $token): array
{
- $contextKey = "__auth.guards.{$this->name}.payload." . md5($token);
+ $contextKey = "__auth.guards.{$this->name}.payload." . hash('xxh128', $token);
return CoroutineContext::getOrSet($contextKey, fn () => $this->jwtManager->decode($token));
}
@@ -279,7 +279,7 @@ protected function getContextKeyForToken(?string $token): string
return "__auth.guards.{$this->name}.user.default";
}
- return "__auth.guards.{$this->name}.user." . md5($token);
+ return "__auth.guards.{$this->name}.user." . hash('xxh128', $token);
}
/**
diff --git a/src/queue/src/Middleware/RateLimited.php b/src/queue/src/Middleware/RateLimited.php
index 095cb10fd..4dde6e813 100644
--- a/src/queue/src/Middleware/RateLimited.php
+++ b/src/queue/src/Middleware/RateLimited.php
@@ -67,7 +67,7 @@ public function handle(mixed $job, callable $next): mixed
$next,
Collection::make(Arr::wrap($limiterResponse))->map(function ($limit) {
return (object) [
- 'key' => md5($this->limiterName . $limit->key),
+ 'key' => hash('xxh128', $this->limiterName . $limit->key),
'maxAttempts' => $limit->maxAttempts,
'decaySeconds' => $limit->decaySeconds,
];
diff --git a/src/routing/src/Middleware/ThrottleRequests.php b/src/routing/src/Middleware/ThrottleRequests.php
index 5e81b1041..d247ca29f 100644
--- a/src/routing/src/Middleware/ThrottleRequests.php
+++ b/src/routing/src/Middleware/ThrottleRequests.php
@@ -109,7 +109,7 @@ protected function handleRequestUsingNamedLimiter(Request $request, Closure $nex
$next,
Collection::wrap($limiterResponse)->map(function ($limit) use ($limiterName) {
return (object) [
- 'key' => self::$shouldHashKeys ? md5($limiterName . $limit->key) : $limiterName . ':' . $limit->key,
+ 'key' => self::$shouldHashKeys ? hash('xxh128', $limiterName . $limit->key) : $limiterName . ':' . $limit->key,
'maxAttempts' => $limit->maxAttempts,
'decaySeconds' => $limit->decaySeconds,
'afterCallback' => $limit->afterCallback,
@@ -274,7 +274,7 @@ protected function calculateRemainingAttempts(string $key, int $maxAttempts, ?in
*/
private function formatIdentifier(string $value): string
{
- return self::$shouldHashKeys ? sha1($value) : $value;
+ return self::$shouldHashKeys ? hash('xxh128', $value) : $value;
}
/**
diff --git a/src/sanctum/src/SanctumGuard.php b/src/sanctum/src/SanctumGuard.php
index 59a3ff7b4..690e7e6b1 100644
--- a/src/sanctum/src/SanctumGuard.php
+++ b/src/sanctum/src/SanctumGuard.php
@@ -273,7 +273,7 @@ protected function getContextKeyForToken(?string $token): string
return "__auth.guards.{$this->name}.user.default";
}
- return "__auth.guards.{$this->name}.user." . md5($token);
+ return "__auth.guards.{$this->name}.user." . hash('xxh128', $token);
}
/**
diff --git a/src/sentry/src/Features/ConsoleSchedulingFeature.php b/src/sentry/src/Features/ConsoleSchedulingFeature.php
index d18f50b24..85834904a 100644
--- a/src/sentry/src/Features/ConsoleSchedulingFeature.php
+++ b/src/sentry/src/Features/ConsoleSchedulingFeature.php
@@ -276,7 +276,7 @@ private function forgetCheckIn(string $cacheKey): void
private function buildCacheKey(string $mutex, string $slug): string
{
// We use the mutex name as part of the cache key to avoid collisions between the same commands with the same schedule but with different slugs
- return 'sentry:checkIn:' . sha1("{$mutex}:{$slug}");
+ return 'sentry:checkIn:' . hash('xxh128', "{$mutex}:{$slug}");
}
private function makeSlugForScheduled(SchedulingEvent $scheduled): string
diff --git a/src/support/src/Facades/DB.php b/src/support/src/Facades/DB.php
index 323fc9c68..953cf77dd 100644
--- a/src/support/src/Facades/DB.php
+++ b/src/support/src/Facades/DB.php
@@ -13,7 +13,6 @@
/**
* @method static \Hypervel\Database\ConnectionInterface connection(\UnitEnum|string|null $name = null)
* @method static \Hypervel\Database\ConnectionInterface build(array $config)
- * @method static string calculateDynamicConnectionName(array $config)
* @method static \Hypervel\Database\ConnectionInterface connectUsing(string $name, array $config, bool $force = false)
* @method static void purge(\UnitEnum|string|null $name = null)
* @method static void disconnect(\UnitEnum|string|null $name = null)
diff --git a/src/support/src/Facades/File.php b/src/support/src/Facades/File.php
index 00aa82d94..9f5d02f5d 100644
--- a/src/support/src/Facades/File.php
+++ b/src/support/src/Facades/File.php
@@ -13,7 +13,7 @@
* @method static mixed getRequire(string $path, array $data = [])
* @method static mixed requireOnce(string $path, array $data = [])
* @method static \Hypervel\Support\LazyCollection lines(string $path)
- * @method static string|false hash(string $path, string $algorithm = 'md5')
+ * @method static string|false hash(string $path, string $algorithm = 'xxh128')
* @method static bool|int put(string $path, resource|string $contents, bool $lock = false)
* @method static void replace(string $path, string $content, int|null $mode = null)
* @method static void replaceInFile(array|string $search, array|string $replace, string $path)
diff --git a/src/telescope/src/IncomingExceptionEntry.php b/src/telescope/src/IncomingExceptionEntry.php
index 22cd3f106..a63cb33eb 100644
--- a/src/telescope/src/IncomingExceptionEntry.php
+++ b/src/telescope/src/IncomingExceptionEntry.php
@@ -42,6 +42,6 @@ public function isException(): bool
*/
public function familyHash(): string
{
- return md5($this->content['file'] . $this->content['line']);
+ return hash('xxh128', $this->content['file'] . $this->content['line']);
}
}
diff --git a/src/telescope/src/Watchers/QueryWatcher.php b/src/telescope/src/Watchers/QueryWatcher.php
index 496417f15..5cfa3e50b 100644
--- a/src/telescope/src/Watchers/QueryWatcher.php
+++ b/src/telescope/src/Watchers/QueryWatcher.php
@@ -64,7 +64,7 @@ protected function tags(QueryExecuted $event): array
*/
public function familyHash(QueryExecuted $event): string
{
- return md5($event->sql);
+ return hash('xxh128', $event->sql);
}
/**
diff --git a/src/view/src/Compilers/BladeCompiler.php b/src/view/src/Compilers/BladeCompiler.php
index 59b8532a3..960b58e44 100644
--- a/src/view/src/Compilers/BladeCompiler.php
+++ b/src/view/src/Compilers/BladeCompiler.php
@@ -759,7 +759,7 @@ public function getClassComponentAliases(): array
*/
public function anonymousComponentPath(string $path, ?string $prefix = null): void
{
- $prefixHash = md5($prefix ?: $path);
+ $prefixHash = hash('xxh128', $prefix ?: $path);
$this->anonymousComponentPaths[] = [
'path' => $path,
diff --git a/src/watcher/README.md b/src/watcher/README.md
index 6f2482a1f..d608296e2 100644
--- a/src/watcher/README.md
+++ b/src/watcher/README.md
@@ -38,7 +38,7 @@ Glob patterns support `*` (single directory segment), `**` (recursive), `?` (sin
| Driver | Description |
|--------|-------------|
-| `ScanFileDriver` | Cross-platform, polls files using MD5 checksums |
+| `ScanFileDriver` | Cross-platform, polls files using file hashes |
| `FindDriver` | Uses `find -mmin` (Linux) or `gfind` (macOS via Homebrew) |
| `FindNewerDriver` | Uses `find -newer` with a reference file for comparison |
| `FswatchDriver` | Uses `fswatch` (macOS native or Linux via `apt`/`brew`) |
diff --git a/src/watcher/config/watcher.php b/src/watcher/config/watcher.php
index c32bcf4c1..b435f3720 100644
--- a/src/watcher/config/watcher.php
+++ b/src/watcher/config/watcher.php
@@ -12,7 +12,7 @@
|
| The driver used to detect file changes. Available drivers:
|
- | - ScanFileDriver: Cross-platform, polls files using MD5 checksums.
+ | - ScanFileDriver: Cross-platform, polls files using file hashes.
| - FindDriver: Uses `find -mmin` (Linux) or `gfind` (macOS via Homebrew).
| - FindNewerDriver: Uses `find -newer` with a reference file for comparison.
| - FswatchDriver: Uses `fswatch` (macOS native or Linux via `apt`/`brew`).
diff --git a/src/watcher/src/Driver/ScanFileDriver.php b/src/watcher/src/Driver/ScanFileDriver.php
index 96c27fd20..d2710b1c5 100644
--- a/src/watcher/src/Driver/ScanFileDriver.php
+++ b/src/watcher/src/Driver/ScanFileDriver.php
@@ -14,67 +14,86 @@ class ScanFileDriver extends AbstractDriver
{
protected Filesystem $filesystem;
- protected ?array $lastMD5 = null;
+ /**
+ * @var null|array
+ */
+ protected ?array $lastFileHashes = null;
- public function __construct(protected Option $option, private StdoutLoggerInterface $logger)
- {
+ public function __construct(
+ protected Option $option,
+ private StdoutLoggerInterface $logger,
+ ?Filesystem $filesystem = null
+ ) {
parent::__construct($option);
- $this->filesystem = new Filesystem;
+
+ $this->filesystem = $filesystem ?? new Filesystem;
}
/**
- * Watch for file changes by polling MD5 checksums.
+ * Watch for file changes by polling file hashes.
*/
public function watch(Channel $channel): void
{
$seconds = $this->option->getScanIntervalSeconds();
$this->timerId = $this->timer->tick($seconds, function () use ($channel) {
- $currentMD5 = $this->getWatchMD5();
- if ($this->lastMD5 && $this->lastMD5 !== $currentMD5) {
- // Added files (in current but not in last).
- $addedFiles = array_diff_key($currentMD5, $this->lastMD5);
- foreach ($addedFiles as $pathName => $md5) {
- $channel->push($pathName);
- }
+ $this->processFileHashes($channel, $this->getWatchFileHashes());
+ });
+ }
- // Deleted files (in last but not in current).
- $deletedFiles = array_diff_key($this->lastMD5, $currentMD5);
+ /**
+ * Process a new file hash snapshot.
+ *
+ * @param array $currentFileHashes
+ */
+ protected function processFileHashes(Channel $channel, array $currentFileHashes): void
+ {
+ if ($this->lastFileHashes !== null && $this->lastFileHashes !== $currentFileHashes) {
+ // Added files (in current but not in last).
+ $addedFiles = array_diff_key($currentFileHashes, $this->lastFileHashes);
+ foreach (array_keys($addedFiles) as $pathName) {
+ $channel->push($pathName);
+ }
+
+ // Deleted files (in last but not in current).
+ $deletedFiles = array_diff_key($this->lastFileHashes, $currentFileHashes);
- // Modified files (same path, different hash).
- $modifiedFiles = [];
- foreach ($currentMD5 as $pathName => $md5) {
- if (isset($this->lastMD5[$pathName]) && $this->lastMD5[$pathName] !== $md5) {
- $modifiedFiles[] = $pathName;
- }
+ // Modified files (same path, different hash).
+ $modifiedFiles = [];
+ foreach ($currentFileHashes as $pathName => $fileHash) {
+ if (isset($this->lastFileHashes[$pathName]) && $this->lastFileHashes[$pathName] !== $fileHash) {
+ $modifiedFiles[] = $pathName;
}
+ }
- $this->logger->debug(sprintf(
- '%s Watching: Total:%d, Change:%d, Add:%d, Delete:%d.',
- self::class,
- count($currentMD5),
- count($modifiedFiles),
- count($addedFiles),
- count($deletedFiles),
- ));
-
- if (count($deletedFiles) === 0) {
- foreach ($modifiedFiles as $pathName) {
- $channel->push($pathName);
- }
- } else {
- $this->logger->warning('Delete files must be restarted manually to take effect.');
+ $this->logger->debug(sprintf(
+ '%s Watching: Total:%d, Change:%d, Add:%d, Delete:%d.',
+ self::class,
+ count($currentFileHashes),
+ count($modifiedFiles),
+ count($addedFiles),
+ count($deletedFiles),
+ ));
+
+ if (count($deletedFiles) === 0) {
+ foreach ($modifiedFiles as $pathName) {
+ $channel->push($pathName);
}
+ } else {
+ $this->logger->warning('Delete files must be restarted manually to take effect.');
}
- $this->lastMD5 = $currentMD5;
- });
+ }
+
+ $this->lastFileHashes = $currentFileHashes;
}
/**
- * Compute MD5 checksums for all watched files.
+ * Compute hashes for all watched files.
+ *
+ * @return array
*/
- protected function getWatchMD5(): array
+ protected function getWatchFileHashes(): array
{
- $filesMD5 = [];
+ $fileHashes = [];
$basePath = base_path();
// Scan watched directories.
@@ -87,7 +106,10 @@ protected function getWatchMD5(): array
if (! $watchPath->matches($relativePath)) {
continue;
}
- $filesMD5[$pathName] = md5(file_get_contents($pathName));
+ $fileHash = $this->hashFile($pathName);
+ if ($fileHash !== null) {
+ $fileHashes[$pathName] = $fileHash;
+ }
}
}
@@ -95,10 +117,23 @@ protected function getWatchMD5(): array
foreach ($this->option->getFilePaths() as $watchPath) {
$pathName = base_path($watchPath->path);
if (file_exists($pathName)) {
- $filesMD5[$pathName] = md5(file_get_contents($pathName));
+ $fileHash = $this->hashFile($pathName);
+ if ($fileHash !== null) {
+ $fileHashes[$pathName] = $fileHash;
+ }
}
}
- return $filesMD5;
+ return $fileHashes;
+ }
+
+ /**
+ * Hash a watched file.
+ */
+ protected function hashFile(string $path): ?string
+ {
+ $hash = $this->filesystem->hash($path);
+
+ return $hash === false ? null : $hash;
}
}
diff --git a/tests/Auth/AuthGuardTest.php b/tests/Auth/AuthGuardTest.php
index 4f0d03460..b686a4712 100755
--- a/tests/Auth/AuthGuardTest.php
+++ b/tests/Auth/AuthGuardTest.php
@@ -844,7 +844,8 @@ public function testGetNameContainsGuardName()
$guard = $this->getGuard();
$this->assertStringContainsString('default', $guard->getName());
- $this->assertStringStartsWith('login_default_', $guard->getName());
+ $this->assertSame('login_default_' . hash('xxh128', SessionGuard::class), $guard->getName());
+ $this->assertSame('remember_default_' . hash('xxh128', SessionGuard::class), $guard->getRecallerName());
}
protected function getGuard()
diff --git a/tests/Cache/CacheFileStoreTest.php b/tests/Cache/CacheFileStoreTest.php
index f7b9a4510..23737b488 100644
--- a/tests/Cache/CacheFileStoreTest.php
+++ b/tests/Cache/CacheFileStoreTest.php
@@ -29,7 +29,7 @@ public function testNullIsReturnedIfFileDoesntExist()
public function testPutCreatesMissingDirectories()
{
$files = $this->mockFilesystem();
- $hash = sha1('foo');
+ $hash = hash('xxh128', 'foo');
$contents = '0000000000';
$full_dir = __DIR__ . '/' . substr($hash, 0, 2) . '/' . substr($hash, 2, 2);
$files->expects($this->once())->method('makeDirectory')->with($this->equalTo($full_dir), $this->equalTo(0777), $this->equalTo(true));
@@ -43,7 +43,7 @@ public function testPutWillConsiderZeroAsEternalTime()
{
$files = $this->mockFilesystem();
- $hash = sha1('O--L / key');
+ $hash = hash('xxh128', 'O--L / key');
$filePath = __DIR__ . '/' . substr($hash, 0, 2) . '/' . substr($hash, 2, 2) . '/' . $hash;
$ten9s = '9999999999'; // The "forever" time value.
$fileContents = $ten9s . serialize('gold');
@@ -62,7 +62,7 @@ public function testPutWillConsiderBigValuesAsEternalTime()
{
$files = $this->mockFilesystem();
- $hash = sha1('O--L / key');
+ $hash = hash('xxh128', 'O--L / key');
$filePath = __DIR__ . '/' . substr($hash, 0, 2) . '/' . substr($hash, 2, 2) . '/' . $hash;
$ten9s = '9999999999'; // The "forever" time value.
$fileContents = $ten9s . serialize('gold');
@@ -101,7 +101,7 @@ public function testStoreItemProperlyStoresValues()
$store = $this->getMockBuilder(FileStore::class)->onlyMethods(['expiration'])->setConstructorArgs([$files, __DIR__])->getMock();
$store->expects($this->once())->method('expiration')->with($this->equalTo(10))->willReturn(1111111111);
$contents = '1111111111' . serialize('Hello World');
- $hash = sha1('foo');
+ $hash = hash('xxh128', 'foo');
$cache_dir = substr($hash, 0, 2) . '/' . substr($hash, 2, 2);
$files->expects($this->once())->method('put')->with($this->equalTo(__DIR__ . '/' . $cache_dir . '/' . $hash), $this->equalTo($contents))->willReturn(strlen($contents));
$result = $store->put('foo', 'Hello World', 10);
@@ -118,7 +118,7 @@ public function testTouchExtendsTtl()
$key = 'foo';
$content = 'Hello World';
$ttl = 60;
- $hash = sha1($key);
+ $hash = hash('xxh128', $key);
$path = __DIR__ . '/' . substr($hash, 0, 2) . '/' . substr($hash, 2, 2) . '/' . $hash;
$store->expects($this->once())
@@ -146,7 +146,7 @@ public function testStoreItemProperlySetsPermissions()
$files = m::mock(Filesystem::class);
$files->shouldIgnoreMissing();
$store = new FileStore($files, __DIR__, 0644);
- $hash = sha1('foo');
+ $hash = hash('xxh128', 'foo');
$cache_dir = substr($hash, 0, 2) . '/' . substr($hash, 2, 2);
$files->shouldReceive('put')->withArgs([__DIR__ . '/' . $cache_dir . '/' . $hash, m::any(), m::any()])->andReturnUsing(function ($name, $value) {
return strlen($value);
@@ -166,7 +166,7 @@ public function testStoreItemDirectoryProperlySetsPermissions()
$files = m::mock(Filesystem::class);
$files->shouldIgnoreMissing();
$store = new FileStore($files, __DIR__, 0606);
- $hash = sha1('foo');
+ $hash = hash('xxh128', 'foo');
$cache_parent_dir = substr($hash, 0, 2);
$cache_dir = $cache_parent_dir . '/' . substr($hash, 2, 2);
@@ -189,7 +189,7 @@ public function testForeversAreStoredWithHighTimestamp()
{
$files = $this->mockFilesystem();
$contents = '9999999999' . serialize('Hello World');
- $hash = sha1('foo');
+ $hash = hash('xxh128', 'foo');
$cache_dir = substr($hash, 0, 2) . '/' . substr($hash, 2, 2);
$files->expects($this->once())->method('put')->with($this->equalTo(__DIR__ . '/' . $cache_dir . '/' . $hash), $this->equalTo($contents))->willReturn(strlen($contents));
$store = new FileStore($files, __DIR__);
@@ -296,7 +296,7 @@ public function testIncrementDoesNotExtendCacheLife()
$valueAfterIncrement = $expiration . serialize(2);
$store = new FileStore($files, __DIR__);
$files->expects($this->once())->method('get')->willReturn($initialValue);
- $hash = sha1('foo');
+ $hash = hash('xxh128', 'foo');
$cache_dir = substr($hash, 0, 2) . '/' . substr($hash, 2, 2);
$files->expects($this->once())->method('put')->with($this->equalTo(__DIR__ . '/' . $cache_dir . '/' . $hash), $this->equalTo($valueAfterIncrement));
$store->increment('foo');
@@ -305,7 +305,7 @@ public function testIncrementDoesNotExtendCacheLife()
public function testRemoveDeletesFileDoesntExist()
{
$files = $this->mockFilesystem();
- $hash = sha1('foobull');
+ $hash = hash('xxh128', 'foobull');
$cache_dir = substr($hash, 0, 2) . '/' . substr($hash, 2, 2);
$files->expects($this->once())->method('exists')->with($this->equalTo(__DIR__ . '/' . $cache_dir . '/' . $hash))->willReturn(false);
$store = new FileStore($files, __DIR__);
@@ -315,7 +315,7 @@ public function testRemoveDeletesFileDoesntExist()
public function testRemoveDeletesFile()
{
$files = $this->mockFilesystem();
- $hash = sha1('foobar');
+ $hash = hash('xxh128', 'foobar');
$cache_dir = substr($hash, 0, 2) . '/' . substr($hash, 2, 2);
$store = new FileStore($files, __DIR__);
$store->put('foobar', 'Hello Baby', 10);
@@ -465,7 +465,7 @@ protected function mockFilesystem()
protected function getCachePath($key)
{
- $hash = sha1($key);
+ $hash = hash('xxh128', $key);
$cache_dir = substr($hash, 0, 2) . '/' . substr($hash, 2, 2);
return __DIR__ . '/' . $cache_dir . '/' . $hash;
diff --git a/tests/Cache/CacheSwooleStoreTest.php b/tests/Cache/CacheSwooleStoreTest.php
index 707c2dce4..16e5f564a 100644
--- a/tests/Cache/CacheSwooleStoreTest.php
+++ b/tests/Cache/CacheSwooleStoreTest.php
@@ -247,11 +247,11 @@ public function testEvictRecordsWhenMemoryLimitIsReached()
$store = $this->createStore($table);
for ($i = 0; $i < 256; ++$i) {
- $store->put(sha1("key:{$i}"), $i, 100);
+ $store->put(hash('xxh128', "key:{$i}"), $i, 100);
}
- $this->assertNull($store->get(sha1('key:0')));
- $this->assertSame(255, $store->get(sha1('key:255')));
+ $this->assertNull($store->get(hash('xxh128', 'key:0')));
+ $this->assertSame(255, $store->get(hash('xxh128', 'key:255')));
$this->assertLessThanOrEqual(128, $table->count());
}
diff --git a/tests/Cache/Redis/AllTaggedCacheTest.php b/tests/Cache/Redis/AllTaggedCacheTest.php
index 20a0f03aa..70c52bded 100644
--- a/tests/Cache/Redis/AllTaggedCacheTest.php
+++ b/tests/Cache/Redis/AllTaggedCacheTest.php
@@ -28,7 +28,7 @@ public function testTagEntriesCanBeStoredForever(): void
$connection->shouldReceive('pipeline')->once()->andReturn($connection);
- $key = sha1('_all:tag:people:entries|_all:tag:author:entries') . ':name';
+ $key = hash('xxh128', '_all:tag:people:entries|_all:tag:author:entries') . ':name';
// Combined operation: ZADD for both tags + SET (forever uses score -1)
$connection->shouldReceive('zadd')->once()->with('prefix:_all:tag:people:entries', -1, $key)->andReturn($connection);
@@ -51,7 +51,7 @@ public function testTagEntriesCanBeStoredForeverWithNumericValue(): void
$connection->shouldReceive('pipeline')->once()->andReturn($connection);
- $key = sha1('_all:tag:people:entries|_all:tag:author:entries') . ':age';
+ $key = hash('xxh128', '_all:tag:people:entries|_all:tag:author:entries') . ':age';
// Numeric values are NOT serialized (optimization)
$connection->shouldReceive('zadd')->once()->with('prefix:_all:tag:people:entries', -1, $key)->andReturn($connection);
@@ -74,7 +74,7 @@ public function testTagEntriesCanBeIncremented(): void
$connection->shouldReceive('pipeline')->once()->andReturn($connection);
- $key = sha1('_all:tag:votes:entries') . ':person-1';
+ $key = hash('xxh128', '_all:tag:votes:entries') . ':person-1';
// Combined operation: ZADD NX + INCRBY in single pipeline
$connection->shouldReceive('zadd')->once()->with('prefix:_all:tag:votes:entries', ['NX'], -1, $key)->andReturn($connection);
@@ -96,7 +96,7 @@ public function testTagEntriesCanBeDecremented(): void
$connection->shouldReceive('pipeline')->once()->andReturn($connection);
- $key = sha1('_all:tag:votes:entries') . ':person-1';
+ $key = hash('xxh128', '_all:tag:votes:entries') . ':person-1';
// Combined operation: ZADD NX + DECRBY in single pipeline
$connection->shouldReceive('zadd')->once()->with('prefix:_all:tag:votes:entries', ['NX'], -1, $key)->andReturn($connection);
@@ -138,7 +138,7 @@ public function testPut(): void
$connection->shouldReceive('pipeline')->once()->andReturn($connection);
- $key = sha1('_all:tag:people:entries|_all:tag:author:entries') . ':name';
+ $key = hash('xxh128', '_all:tag:people:entries|_all:tag:author:entries') . ':name';
$expectedScore = now()->timestamp + 5;
// Combined operation: ZADD for both tags + SETEX in single pipeline
@@ -162,7 +162,7 @@ public function testPutWithNumericValue(): void
$connection->shouldReceive('pipeline')->once()->andReturn($connection);
- $key = sha1('_all:tag:people:entries|_all:tag:author:entries') . ':age';
+ $key = hash('xxh128', '_all:tag:people:entries|_all:tag:author:entries') . ':age';
$expectedScore = now()->timestamp + 5;
// Numeric values are NOT serialized
@@ -186,7 +186,7 @@ public function testPutWithArray(): void
$connection->shouldReceive('pipeline')->once()->andReturn($connection);
- $namespace = sha1('_all:tag:people:entries|_all:tag:author:entries') . ':';
+ $namespace = hash('xxh128', '_all:tag:people:entries|_all:tag:author:entries') . ':';
$expectedScore = now()->timestamp + 5;
// PutMany uses variadic ZADD: one command per tag with all keys as members
@@ -266,7 +266,7 @@ public function testPutNullTtlCallsForever(): void
$connection->shouldReceive('pipeline')->once()->andReturn($connection);
- $key = sha1('_all:tag:users:entries') . ':name';
+ $key = hash('xxh128', '_all:tag:users:entries') . ':name';
// Null TTL should call forever (ZADD with -1 + SET)
$connection->shouldReceive('zadd')->once()->with('prefix:_all:tag:users:entries', -1, $key)->andReturn($connection);
@@ -286,7 +286,7 @@ public function testPutZeroTtlDeletesKey(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:users:entries') . ':name';
+ $key = hash('xxh128', '_all:tag:users:entries') . ':name';
// Zero TTL should delete the key (Forget operation uses connection)
$connection->shouldReceive('del')
@@ -309,7 +309,7 @@ public function testIncrementWithCustomValue(): void
$connection->shouldReceive('pipeline')->once()->andReturn($connection);
- $key = sha1('_all:tag:counters:entries') . ':hits';
+ $key = hash('xxh128', '_all:tag:counters:entries') . ':hits';
$connection->shouldReceive('zadd')->once()->with('prefix:_all:tag:counters:entries', ['NX'], -1, $key)->andReturn($connection);
$connection->shouldReceive('incrby')->once()->with("prefix:{$key}", 5)->andReturn($connection);
@@ -330,7 +330,7 @@ public function testDecrementWithCustomValue(): void
$connection->shouldReceive('pipeline')->once()->andReturn($connection);
- $key = sha1('_all:tag:counters:entries') . ':stock';
+ $key = hash('xxh128', '_all:tag:counters:entries') . ':stock';
$connection->shouldReceive('zadd')->once()->with('prefix:_all:tag:counters:entries', ['NX'], -1, $key)->andReturn($connection);
$connection->shouldReceive('decrby')->once()->with("prefix:{$key}", 3)->andReturn($connection);
@@ -349,7 +349,7 @@ public function testRememberReturnsExistingValueOnCacheHit(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:users:entries') . ':profile';
+ $key = hash('xxh128', '_all:tag:users:entries') . ':profile';
// Remember operation uses connection->get() directly
$connection->shouldReceive('get')
@@ -370,7 +370,7 @@ public function testRememberCallsCallbackAndStoresValueOnMiss(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:users:entries') . ':profile';
+ $key = hash('xxh128', '_all:tag:users:entries') . ':profile';
$expectedScore = now()->timestamp + 60;
// Cache miss
@@ -404,7 +404,7 @@ public function testRememberDoesNotCallCallbackOnCacheHit(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:users:entries') . ':data';
+ $key = hash('xxh128', '_all:tag:users:entries') . ':data';
$connection->shouldReceive('get')
->once()
@@ -426,7 +426,7 @@ public function testRememberDoesNotCallCallbackOnCacheHit(): void
public function testRememberNullableStoresAndReturnsNonNullValue(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:users:entries') . ':profile';
+ $key = hash('xxh128', '_all:tag:users:entries') . ':profile';
$expectedScore = now()->timestamp + 60;
$connection->shouldReceive('get')->once()->with("prefix:{$key}")->andReturnNull();
@@ -444,7 +444,7 @@ public function testRememberNullableStoresAndReturnsNonNullValue(): void
public function testRememberNullableStoresSentinelWhenCallbackReturnsNull(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:users:entries') . ':profile';
+ $key = hash('xxh128', '_all:tag:users:entries') . ':profile';
$expectedScore = now()->timestamp + 60;
$connection->shouldReceive('get')->once()->with("prefix:{$key}")->andReturnNull();
@@ -469,7 +469,7 @@ public function testRememberNullableStoresSentinelWhenCallbackReturnsNull(): voi
public function testRememberNullableReturnsNullOnSentinelHitWithoutInvokingCallback(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:users:entries') . ':profile';
+ $key = hash('xxh128', '_all:tag:users:entries') . ':profile';
$connection->shouldReceive('get')
->once()
@@ -490,7 +490,7 @@ public function testRememberNullableReturnsNullOnSentinelHitWithoutInvokingCallb
public function testRememberNullableFiresCacheHitWithNullPayloadOnSentinelHit(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:users:entries') . ':profile';
+ $key = hash('xxh128', '_all:tag:users:entries') . ':profile';
$connection->shouldReceive('get')
->once()
@@ -520,7 +520,7 @@ public function testRememberNullableFiresCacheHitWithNullPayloadOnSentinelHit():
public function testRememberNullableFiresKeyWrittenWithNullPayloadOnCacheMiss(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:users:entries') . ':profile';
+ $key = hash('xxh128', '_all:tag:users:entries') . ':profile';
$expectedScore = now()->timestamp + 60;
$connection->shouldReceive('get')->once()->with("prefix:{$key}")->andReturnNull();
@@ -556,7 +556,7 @@ public function testRememberNullableFiresKeyWrittenWithNullPayloadOnCacheMiss():
public function testPlainRememberUnwrapsSentinelOnCachedNullHit(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:users:entries') . ':profile';
+ $key = hash('xxh128', '_all:tag:users:entries') . ':profile';
$connection->shouldReceive('get')
->once()
@@ -581,7 +581,7 @@ public function testRememberForeverReturnsExistingValueOnCacheHit(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:config:entries') . ':settings';
+ $key = hash('xxh128', '_all:tag:config:entries') . ':settings';
$connection->shouldReceive('get')
->once()
@@ -601,7 +601,7 @@ public function testRememberForeverCallsCallbackAndStoresValueOnMiss(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:config:entries') . ':settings';
+ $key = hash('xxh128', '_all:tag:config:entries') . ':settings';
// Cache miss
$connection->shouldReceive('get')
@@ -624,7 +624,7 @@ public function testRememberForeverCallsCallbackAndStoresValueOnMiss(): void
public function testRememberForeverNullableStoresSentinelWhenCallbackReturnsNull(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:config:entries') . ':settings';
+ $key = hash('xxh128', '_all:tag:config:entries') . ':settings';
$connection->shouldReceive('get')->once()->with("prefix:{$key}")->andReturnNull();
$connection->shouldReceive('pipeline')->once()->andReturn($connection);
@@ -647,7 +647,7 @@ public function testRememberForeverNullableStoresSentinelWhenCallbackReturnsNull
public function testSearNullableDelegatesToRememberForeverNullable(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:config:entries') . ':settings';
+ $key = hash('xxh128', '_all:tag:config:entries') . ':settings';
$connection->shouldReceive('get')->once()->with("prefix:{$key}")->andReturnNull();
$connection->shouldReceive('pipeline')->once()->andReturn($connection);
@@ -673,7 +673,7 @@ public function testSearNullableDelegatesToRememberForeverNullable(): void
public function testPlainRememberForeverUnwrapsSentinelOnCachedNullHit(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:config:entries') . ':settings';
+ $key = hash('xxh128', '_all:tag:config:entries') . ':settings';
$connection->shouldReceive('get')
->once()
@@ -698,7 +698,7 @@ public function testRememberPropagatesExceptionFromCallback(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:users:entries') . ':data';
+ $key = hash('xxh128', '_all:tag:users:entries') . ':data';
$connection->shouldReceive('get')
->once()
@@ -721,7 +721,7 @@ public function testRememberForeverPropagatesExceptionFromCallback(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:config:entries') . ':data';
+ $key = hash('xxh128', '_all:tag:config:entries') . ':data';
$connection->shouldReceive('get')
->once()
@@ -744,7 +744,7 @@ public function testRememberWithMultipleTags(): void
{
$connection = $this->mockConnection();
- $key = sha1('_all:tag:users:entries|_all:tag:posts:entries') . ':activity';
+ $key = hash('xxh128', '_all:tag:users:entries|_all:tag:posts:entries') . ':activity';
$expectedScore = now()->timestamp + 120;
// Cache miss
@@ -769,8 +769,8 @@ public function testRememberWithMultipleTags(): void
public function testFlexibleNullableReturnsNullOnFreshSentinelHit(): void
{
$connection = $this->mockConnection();
- $valueKey = sha1('_all:tag:posts:entries') . ':digest';
- $markerKey = sha1('_all:tag:posts:entries') . ':hypervel:cache:flexible:created:digest';
+ $valueKey = hash('xxh128', '_all:tag:posts:entries') . ':digest';
+ $markerKey = hash('xxh128', '_all:tag:posts:entries') . ':hypervel:cache:flexible:created:digest';
$now = now()->timestamp;
// flexible() reads both keys via a single manyRaw() → store->many() → MGET.
diff --git a/tests/Cache/Redis/Console/DoctorCommandTest.php b/tests/Cache/Redis/Console/DoctorCommandTest.php
index 7e50065eb..9b3606c16 100644
--- a/tests/Cache/Redis/Console/DoctorCommandTest.php
+++ b/tests/Cache/Redis/Console/DoctorCommandTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Cache\Redis\Console;
+use Closure;
use Hypervel\Cache\CacheManager;
use Hypervel\Cache\Redis\Console\Doctor\DoctorContext;
use Hypervel\Cache\Redis\Console\DoctorCommand;
@@ -15,6 +16,7 @@
use Hypervel\Contracts\Cache\Factory as CacheContract;
use Hypervel\Contracts\Cache\Store;
use Hypervel\Redis\PhpRedisConnection;
+use Hypervel\Redis\RedisConnection;
use Hypervel\Testbench\TestCase;
use Mockery as m;
use Symfony\Component\Console\Input\ArrayInput;
@@ -173,6 +175,93 @@ protected function runCleanupVerification(DoctorContext $context): void
$this->assertStringContainsString('custom-redis', $outputText);
}
+ public function testDoctorRunsChecksWhileRedisConnectionIsBorrowed(): void
+ {
+ $config = m::mock(ConfigRepository::class);
+ $config->shouldReceive('get')
+ ->with('cache.default', 'file')
+ ->andReturn('redis');
+
+ $this->app->instance('config', $config);
+
+ $borrowedConnection = false;
+ $assertConnectionBorrowed = function () use (&$borrowedConnection): void {
+ $this->assertTrue($borrowedConnection);
+ };
+
+ $connection = m::mock(PhpRedisConnection::class);
+ $connection->shouldReceive('info')
+ ->once()
+ ->with('server')
+ ->andReturn(['redis_version' => '7.0.0']);
+
+ $context = m::mock(StoreContext::class);
+ $context->shouldReceive('withConnection')
+ ->twice()
+ ->andReturnUsing(function (callable $callback) use (&$borrowedConnection, $connection) {
+ $borrowedConnection = true;
+
+ try {
+ return $callback($connection);
+ } finally {
+ $borrowedConnection = false;
+ }
+ });
+
+ $store = m::mock(RedisStore::class);
+ $store->shouldReceive('getTagMode')->andReturn(TagMode::Any);
+ $store->shouldReceive('getContext')->andReturn($context);
+ $store->shouldReceive('getPrefix')->andReturn('cache:');
+
+ $repository = m::mock(Repository::class);
+ $repository->shouldReceive('getStore')->andReturn($store);
+
+ $cacheManager = m::mock(CacheManager::class);
+ $cacheManager->shouldReceive('store')
+ ->with('redis')
+ ->andReturn($repository);
+
+ $this->app->instance(CacheContract::class, $cacheManager);
+
+ $command = new class($assertConnectionBorrowed) extends DoctorCommand {
+ public function __construct(private readonly Closure $assertConnectionBorrowed)
+ {
+ parent::__construct();
+ }
+
+ protected function runEnvironmentChecks(
+ string $storeName,
+ RedisStore $store,
+ string $taggingMode,
+ RedisConnection $redis
+ ): bool {
+ ($this->assertConnectionBorrowed)();
+
+ return true;
+ }
+
+ protected function runFunctionalChecks(DoctorContext $context): void
+ {
+ ($this->assertConnectionBorrowed)();
+ }
+
+ protected function runCleanupVerification(DoctorContext $context): void
+ {
+ ($this->assertConnectionBorrowed)();
+ }
+
+ protected function cleanup(DoctorContext $context, bool $silent = false): void
+ {
+ ($this->assertConnectionBorrowed)();
+ }
+ };
+
+ $command->setHypervel($this->app);
+ $result = $command->run(new ArrayInput(['--store' => 'redis']), new BufferedOutput);
+
+ $this->assertSame(0, $result);
+ }
+
public function testDoctorDisplaysTagMode(): void
{
$config = m::mock(ConfigRepository::class);
diff --git a/tests/Console/Scheduling/CallbackEventTest.php b/tests/Console/Scheduling/CallbackEventTest.php
index 23158041a..83eac75f2 100644
--- a/tests/Console/Scheduling/CallbackEventTest.php
+++ b/tests/Console/Scheduling/CallbackEventTest.php
@@ -148,14 +148,14 @@ public function testMutexNameUsesDescription(): void
$event = new CallbackEvent($this->mutex, fn () => true);
$event->name('unique-task-name');
- $this->assertSame('framework/schedule-' . sha1('unique-task-name'), $event->mutexName());
+ $this->assertSame('framework/schedule-' . hash('xxh128', 'unique-task-name'), $event->mutexName());
}
public function testMutexNameWithoutDescription(): void
{
$event = new CallbackEvent($this->mutex, fn () => true);
- $this->assertSame('framework/schedule-' . sha1(''), $event->mutexName());
+ $this->assertSame('framework/schedule-' . hash('xxh128', ''), $event->mutexName());
}
public function testShouldSkipDueToOverlappingReturnsFalseWithoutDescription(): void
diff --git a/tests/Console/Scheduling/EventTest.php b/tests/Console/Scheduling/EventTest.php
index b1c213a24..e81a8a495 100644
--- a/tests/Console/Scheduling/EventTest.php
+++ b/tests/Console/Scheduling/EventTest.php
@@ -268,7 +268,10 @@ public function testCustomMutexName()
$event = new Event(m::mock(EventMutex::class), 'php -i');
$event->description('Fancy command description');
- $this->assertSame('framework' . DIRECTORY_SEPARATOR . 'schedule-eeb46c93d45e928d62aaf684d727e213b7094822', $event->mutexName());
+ $this->assertSame(
+ 'framework' . DIRECTORY_SEPARATOR . 'schedule-' . hash('xxh128', $event->getExpression() . Event::normalizeCommand('php -i')),
+ $event->mutexName()
+ );
$event->createMutexNameUsing(function (Event $event) {
return Str::slug($event->description);
diff --git a/tests/Filesystem/FilesystemTest.php b/tests/Filesystem/FilesystemTest.php
index 9e20e3231..5b6cf6374 100755
--- a/tests/Filesystem/FilesystemTest.php
+++ b/tests/Filesystem/FilesystemTest.php
@@ -627,13 +627,14 @@ public function testHashWithDefaultValue()
{
file_put_contents($this->tempDir . '/foo.txt', 'foo');
$filesystem = new Filesystem;
- $this->assertSame('acbd18db4cc2f85cedef654fccc4a4d8', $filesystem->hash($this->tempDir . '/foo.txt'));
+ $this->assertSame('79aef92e83454121ab6e5f64077e7d8a', $filesystem->hash($this->tempDir . '/foo.txt'));
}
public function testHash()
{
file_put_contents($this->tempDir . '/foo.txt', 'foo');
$filesystem = new Filesystem;
+ $this->assertSame('acbd18db4cc2f85cedef654fccc4a4d8', $filesystem->hash($this->tempDir . '/foo.txt', 'md5'));
$this->assertSame('0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33', $filesystem->hash($this->tempDir . '/foo.txt', 'sha1'));
$this->assertSame('76d3bc41c9f588f7fcd0d5bf4718f8f84b1c41b20882703100b9eb9413807c01', $filesystem->hash($this->tempDir . '/foo.txt', 'sha3-256'));
}
diff --git a/tests/Foundation/FoundationViteTest.php b/tests/Foundation/FoundationViteTest.php
index 7dd7f9055..639f0f526 100644
--- a/tests/Foundation/FoundationViteTest.php
+++ b/tests/Foundation/FoundationViteTest.php
@@ -683,7 +683,7 @@ public function testItGetsHashInBuildMode()
{
$this->makeViteManifest(['a.js' => ['src' => 'a.js']]);
- $this->assertSame('98ca5a789544599b562c9978f3147a0f', ViteFacade::manifestHash());
+ $this->assertSame('4f73e7c072a5410b92846df2052c7839', ViteFacade::manifestHash());
}
public function testItGetsDifferentHashesForDifferentManifestsInBuildMode()
@@ -691,8 +691,8 @@ public function testItGetsDifferentHashesForDifferentManifestsInBuildMode()
$this->makeViteManifest(['a.js' => ['src' => 'a.js']]);
$this->makeViteManifest(['b.js' => ['src' => 'b.js']], 'admin');
- $this->assertSame('98ca5a789544599b562c9978f3147a0f', ViteFacade::manifestHash());
- $this->assertSame('928a60835978bae84e5381fbb08a38b2', ViteFacade::manifestHash('admin'));
+ $this->assertSame('4f73e7c072a5410b92846df2052c7839', ViteFacade::manifestHash());
+ $this->assertSame('fe4db42397a18ffd2e0638cca3a85567', ViteFacade::manifestHash('admin'));
}
public function testViteCanSetEntryPointsWithFluentBuilder()
diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php
index e3b7f247b..6d4bfa17d 100644
--- a/tests/Http/HttpRequestTest.php
+++ b/tests/Http/HttpRequestTest.php
@@ -5,7 +5,9 @@
namespace Hypervel\Tests\Http;
use Hypervel\Http\Request;
+use Hypervel\Routing\Route;
use Hypervel\Tests\TestCase;
+use RuntimeException;
class HttpRequestTest extends TestCase
{
@@ -36,4 +38,30 @@ public function testAcceptsMarkdown()
$request = Request::create('/', 'GET', [], [], [], ['HTTP_ACCEPT' => 'application/json']);
$this->assertFalse($request->acceptsMarkdown());
}
+
+ public function testFingerprintReturnsXxh128HashForRouteAndIp(): void
+ {
+ $request = Request::create('/users', 'GET', [], [], [], ['REMOTE_ADDR' => '127.0.0.1']);
+ $route = new Route(['GET', 'HEAD'], '/users', ['uses' => fn () => null]);
+
+ $request->setRouteResolver(fn () => $route);
+
+ $this->assertSame(
+ hash('xxh128', implode('|', array_merge(
+ $route->methods(),
+ [$route->getDomain(), $route->uri(), $request->ip()]
+ ))),
+ $request->fingerprint()
+ );
+ }
+
+ public function testFingerprintThrowsWhenRouteIsUnavailable(): void
+ {
+ $request = Request::create('/users', 'GET');
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Unable to generate fingerprint. Route unavailable.');
+
+ $request->fingerprint();
+ }
}
diff --git a/tests/Integration/Cache/FailoverStoreTest.php b/tests/Integration/Cache/FailoverStoreTest.php
index 19c945dff..b9fc781d0 100644
--- a/tests/Integration/Cache/FailoverStoreTest.php
+++ b/tests/Integration/Cache/FailoverStoreTest.php
@@ -64,6 +64,10 @@ public function testFailoverCacheDispatchesEventOnlyOnce()
public function testSeparateFailoverStoresDoNotShareFailureEventsForTheSameFailingStore()
{
$events = m::mock(Dispatcher::class);
+ $events->shouldReceive('hasListeners')
+ ->twice()
+ ->with(CacheFailedOver::class)
+ ->andReturn(true);
$events->shouldReceive('dispatch')
->twice()
->with(m::on(fn (object $event) => $event instanceof CacheFailedOver && $event->storeName === 'failing'));
@@ -100,6 +104,42 @@ public function testSeparateFailoverStoresDoNotShareFailureEventsForTheSameFaili
$this->assertSame('fallback-b', $storeB->get('test-b'));
}
+ public function testFailoverCacheSkipsFailedOverEventWhenThereAreNoListeners(): void
+ {
+ $events = m::mock(Dispatcher::class);
+ $events->shouldReceive('hasListeners')
+ ->once()
+ ->with(CacheFailedOver::class)
+ ->andReturn(false);
+ $events->shouldNotReceive('dispatch');
+
+ $failingRepository = m::mock(CacheRepository::class);
+ $failingRepository->shouldReceive('getRaw')
+ ->once()
+ ->with('test')
+ ->andThrow(new Exception('The primary store failed.'));
+
+ $fallbackRepository = m::mock(CacheRepository::class);
+ $fallbackRepository->shouldReceive('getRaw')
+ ->once()
+ ->with('test')
+ ->andReturn('fallback');
+
+ $cacheManager = m::mock(CacheManager::class);
+ $cacheManager->shouldReceive('store')
+ ->with('failing')
+ ->once()
+ ->andReturn($failingRepository);
+ $cacheManager->shouldReceive('store')
+ ->with('fallback')
+ ->once()
+ ->andReturn($fallbackRepository);
+
+ $store = new FailoverStore($cacheManager, $events, ['failing', 'fallback']);
+
+ $this->assertSame('fallback', $store->get('test'));
+ }
+
public function testNullSentinelRoundTripsThroughFailoverStorePrimary()
{
$primaryRepo = new Repository(new ArrayStore(serializesValues: true));
diff --git a/tests/Integration/Cache/Redis/KeyNamingIntegrationTest.php b/tests/Integration/Cache/Redis/KeyNamingIntegrationTest.php
index 7217bda33..a146d5d0b 100644
--- a/tests/Integration/Cache/Redis/KeyNamingIntegrationTest.php
+++ b/tests/Integration/Cache/Redis/KeyNamingIntegrationTest.php
@@ -4,8 +4,11 @@
namespace Hypervel\Tests\Integration\Cache\Redis;
+use Hypervel\Cache\Redis\Console\Doctor\DoctorContext;
use Hypervel\Cache\Redis\Support\StoreContext;
+use Hypervel\Cache\Repository;
use Hypervel\Cache\TagMode;
+use Hypervel\Redis\RedisConnection;
use Hypervel\Support\Facades\Cache;
use Redis;
@@ -75,11 +78,44 @@ public function testAllModeStoresNamespacedKeyInZset(): void
Cache::tags(['mytag'])->put('mykey', 'value', 60);
- // In all mode, the ZSET stores the namespaced key (sha1 of tags + key)
+ // In all mode, the ZSET stores the namespaced key (xxh128 of tags + key)
$entries = $this->getAllModeTagEntries('mytag');
$this->assertCount(1, $entries);
}
+ public function testDoctorContextNamespacedKeyMatchesAllModeRedisTaggedKey(): void
+ {
+ $this->setTagMode(TagMode::All);
+
+ $cache = Cache::store('redis');
+ $this->assertInstanceOf(Repository::class, $cache);
+
+ $taggedCache = $cache->tags(['beta', 'alpha']);
+ $taggedCache->put('key', 'value', 60);
+
+ $store = $this->store();
+
+ $store->getContext()->withConnection(function (RedisConnection $redis) use ($cache, $store, $taggedCache): void {
+ $context = new DoctorContext(
+ cache: $cache,
+ store: $store,
+ redis: $redis,
+ cachePrefix: $store->getPrefix(),
+ storeName: 'redis',
+ );
+
+ $namespacedKey = $context->namespacedKey(['beta', 'alpha'], 'key');
+ $reversedNamespacedKey = $context->namespacedKey(['alpha', 'beta'], 'key');
+
+ $this->assertSame($taggedCache->taggedItemKey('key'), $namespacedKey);
+ $this->assertNotSame($namespacedKey, $reversedNamespacedKey);
+ $this->assertRedisKeyExists($this->getCachePrefix() . $namespacedKey);
+ $this->assertTrue($this->allModeTagHasEntry('beta', $namespacedKey));
+ $this->assertTrue($this->allModeTagHasEntry('alpha', $namespacedKey));
+ $this->assertFalse($this->allModeTagHasEntry('beta', $reversedNamespacedKey));
+ });
+ }
+
public function testAllModeZsetScoreIsExpiryTimestamp(): void
{
$this->setTagMode(TagMode::All);
diff --git a/tests/Integration/Cache/Redis/TaggedOperationsIntegrationTest.php b/tests/Integration/Cache/Redis/TaggedOperationsIntegrationTest.php
index b6a48cacc..961baa3df 100644
--- a/tests/Integration/Cache/Redis/TaggedOperationsIntegrationTest.php
+++ b/tests/Integration/Cache/Redis/TaggedOperationsIntegrationTest.php
@@ -45,7 +45,7 @@ public function testAllModeStoresNamespacedKeyInZset(): void
$this->assertNotEmpty($entries, 'ZSET should contain entries');
- // The key stored is the namespaced key (sha1 of tag names + key)
+ // The key stored is the namespaced key (xxh128 of tag names + key)
// We can't predict the exact key, but we can verify an entry exists
$this->assertCount(1, $entries);
}
diff --git a/tests/Integration/Database/Sqlite/InMemorySqliteSharedPdoTest.php b/tests/Integration/Database/Sqlite/InMemorySqliteSharedPdoTest.php
index 5fa5d70c0..3b260fdb3 100644
--- a/tests/Integration/Database/Sqlite/InMemorySqliteSharedPdoTest.php
+++ b/tests/Integration/Database/Sqlite/InMemorySqliteSharedPdoTest.php
@@ -87,7 +87,7 @@ public function testIsInMemorySqliteDetection(string $database, bool $expected):
],
];
- $configKey = 'in_memory_test_' . md5($database);
+ $configKey = 'in_memory_test_' . hash('xxh128', $database);
$config->set("database.connections.{$configKey}", $connectionConfig);
$factory = $this->getPoolFactory();
diff --git a/tests/Integration/Redis/RedisConnectorTest.php b/tests/Integration/Redis/RedisConnectorTest.php
index dc80ca7be..bc1f104bd 100644
--- a/tests/Integration/Redis/RedisConnectorTest.php
+++ b/tests/Integration/Redis/RedisConnectorTest.php
@@ -4,6 +4,7 @@
namespace Hypervel\Tests\Integration\Redis;
+use Closure;
use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Foundation\Testing\Concerns\InteractsWithRedis;
use Hypervel\Redis\RedisConnection;
@@ -32,10 +33,10 @@ public function testDefaultConfiguration()
$host = $this->app->make('config')->get('database.redis.default.host');
$port = $this->app->make('config')->get('database.redis.default.port');
- $client = $this->getClient('default');
-
- $this->assertSame($host, $client->getHost());
- $this->assertSame($port, $client->getPort());
+ $this->withClient('default', function (\Redis $client) use ($host, $port): void {
+ $this->assertSame($host, $client->getHost());
+ $this->assertSame($port, $client->getPort());
+ });
}
public function testUrl()
@@ -49,11 +50,11 @@ public function testUrl()
'database' => $this->getParallelRedisDb(),
]);
- $client = $this->getClient($name);
-
- // redis:// URL maps to tcp:// scheme via ConfigurationUrlParser driver aliases
- $this->assertSame("tcp://{$host}", $client->getHost());
- $this->assertEquals($port, $client->getPort());
+ $this->withClient($name, function (\Redis $client) use ($host, $port): void {
+ // redis:// URL maps to tcp:// scheme via ConfigurationUrlParser driver aliases
+ $this->assertSame("tcp://{$host}", $client->getHost());
+ $this->assertEquals($port, $client->getPort());
+ });
}
public function testUrlWithScheme()
@@ -67,10 +68,10 @@ public function testUrlWithScheme()
'database' => $this->getParallelRedisDb(),
]);
- $client = $this->getClient($name);
-
- $this->assertSame("tcp://{$host}", $client->getHost());
- $this->assertEquals($port, $client->getPort());
+ $this->withClient($name, function (\Redis $client) use ($host, $port): void {
+ $this->assertSame("tcp://{$host}", $client->getHost());
+ $this->assertEquals($port, $client->getPort());
+ });
}
public function testScheme()
@@ -86,10 +87,10 @@ public function testScheme()
'database' => $this->getParallelRedisDb(),
]);
- $client = $this->getClient($name);
-
- $this->assertSame("tcp://{$host}", $client->getHost());
- $this->assertEquals($port, $client->getPort());
+ $this->withClient($name, function (\Redis $client) use ($host, $port): void {
+ $this->assertSame("tcp://{$host}", $client->getHost());
+ $this->assertEquals($port, $client->getPort());
+ });
}
public function testPerConnectionPrefixOverridesGlobalPrefix()
@@ -110,18 +111,25 @@ public function testPerConnectionPrefixOverridesGlobalPrefix()
// Must purge + re-resolve since config changed after initial resolution
$this->app->make('redis')->purge($name);
- $client = $this->getClient($name);
-
- $this->assertSame('per_connection_', $client->getOption(\Redis::OPT_PREFIX));
+ $this->withClient($name, function (\Redis $client): void {
+ $this->assertSame('per_connection_', $client->getOption(\Redis::OPT_PREFIX));
+ });
}
/**
- * Get the underlying phpredis client for a named connection.
+ * Execute a callback with the underlying phpredis client for a named connection.
+ *
+ * @param Closure(\Redis): void $callback
*/
- private function getClient(string $name): \Redis
+ private function withClient(string $name, Closure $callback): void
{
- return Redis::connection($name)->withConnection(
- fn (RedisConnection $connection) => $connection->client(),
+ Redis::connection($name)->withConnection(
+ function (RedisConnection $connection) use ($callback): void {
+ $client = $connection->client();
+ $this->assertInstanceOf(\Redis::class, $client);
+
+ $callback($client);
+ },
transform: false
);
}
diff --git a/tests/Routing/ThrottleRequestsTest.php b/tests/Routing/ThrottleRequestsTest.php
index 4ce1ce61d..606c40559 100644
--- a/tests/Routing/ThrottleRequestsTest.php
+++ b/tests/Routing/ThrottleRequestsTest.php
@@ -21,7 +21,7 @@ public function testAuthenticatedIntegerIdentifierIsNormalizedForRequestSignatur
]));
$this->assertSame(
- sha1('123'),
+ hash('xxh128', '123'),
(new ExposesThrottleRequestSignature)->resolveRequestSignatureForTest($request)
);
diff --git a/tests/View/Blade/BladeComponentTagCompilerTest.php b/tests/View/Blade/BladeComponentTagCompilerTest.php
index a6c342370..842e8b8c5 100644
--- a/tests/View/Blade/BladeComponentTagCompilerTest.php
+++ b/tests/View/Blade/BladeComponentTagCompilerTest.php
@@ -659,6 +659,29 @@ public function testClasslessComponentsWithAnonymousComponentPath()
. '@endComponentClass##END-COMPONENT-CLASS##', trim($result));
}
+ public function testAnonymousComponentPathGeneratesXxh128PrefixHash(): void
+ {
+ $expectedHash = hash('xxh128', 'test-directory');
+ $factory = m::mock(Factory::class);
+ $factory->shouldReceive('addNamespace')->once()->with($expectedHash, 'test-directory')->andReturnSelf();
+
+ $container = new TestBladeApplication('base_path');
+ $container->instance(Factory::class, $factory);
+ $container->alias(Factory::class, 'view');
+
+ Container::setInstance($container);
+
+ $this->compiler->anonymousComponentPath('test-directory');
+
+ $this->assertSame([
+ [
+ 'path' => 'test-directory',
+ 'prefix' => null,
+ 'prefixHash' => $expectedHash,
+ ],
+ ], $this->compiler->getAnonymousComponentPaths());
+ }
+
public function testClasslessComponentsWithAnonymousComponentPathComponentName()
{
$this->mockViewFactory(function ($arg) {
@@ -754,7 +777,7 @@ public function testAttributesTreatedAsPropsAreRemovedFromFinalAttributes()
$factory = m::mock(Factory::class);
$factory->shouldReceive('exists')->never();
- $container = new TestBladeApplication('bath_path');
+ $container = new TestBladeApplication('base_path');
$container->instance(Factory::class, $factory);
$container->alias(Factory::class, 'view');
diff --git a/tests/Watcher/Driver/ScanFileDriverTest.php b/tests/Watcher/Driver/ScanFileDriverTest.php
index 9f101f50e..d6b797fd0 100644
--- a/tests/Watcher/Driver/ScanFileDriverTest.php
+++ b/tests/Watcher/Driver/ScanFileDriverTest.php
@@ -5,6 +5,7 @@
namespace Hypervel\Tests\Watcher\Driver;
use Hypervel\Engine\Channel;
+use Hypervel\Filesystem\Filesystem;
use Hypervel\Tests\TestCase;
use Hypervel\Tests\Watcher\Fixtures\ContainerStub;
use Hypervel\Tests\Watcher\Fixtures\ScanFileDriverStub;
@@ -12,6 +13,7 @@
use Hypervel\Watcher\Option;
use Hypervel\Watcher\WatchPath;
use Hypervel\Watcher\WatchPathType;
+use Mockery as m;
class ScanFileDriverTest extends TestCase
{
@@ -52,13 +54,13 @@ public function testAddAndModifyInSameCycleReportsBothCorrectly()
$logger = ContainerStub::getLogger();
$logger->shouldReceive('warning')->andReturn(null);
- // Anonymous stub that returns different MD5 maps on successive calls.
+ // Anonymous stub that returns different file hash maps on successive calls.
// Tick 1: {A, C} — establishes baseline.
// Tick 2: {A, B, C_changed} — B is added, C is modified, A is unchanged.
$driver = new class($option, $logger) extends ScanFileDriver {
private int $callCount = 0;
- protected function getWatchMD5(): array
+ protected function getWatchFileHashes(): array
{
return match (++$this->callCount) {
1 => ['/tmp/A.php' => 'hash_a', '/tmp/C.php' => 'hash_c'],
@@ -89,4 +91,67 @@ protected function getWatchMD5(): array
$channel->close();
}
}
+
+ public function testEmptyBaselineReportsNewFiles(): void
+ {
+ $option = new Option(
+ driver: ScanFileDriver::class,
+ watchPaths: [
+ new WatchPath('/tmp', WatchPathType::Directory),
+ ],
+ scanInterval: 1,
+ );
+
+ $logger = ContainerStub::getLogger();
+ $logger->shouldReceive('warning')->andReturn(null);
+
+ $driver = new class($option, $logger) extends ScanFileDriver {
+ public function process(Channel $channel, array $fileHashes): void
+ {
+ $this->processFileHashes($channel, $fileHashes);
+ }
+ };
+
+ $channel = new Channel(10);
+
+ try {
+ $driver->process($channel, []);
+ $driver->process($channel, ['/tmp/B.php' => 'hash_b']);
+
+ $this->assertSame('/tmp/B.php', $channel->pop(0.1));
+ $this->assertFalse($channel->pop(0.01));
+ } finally {
+ $driver->stop();
+ $channel->close();
+ }
+ }
+
+ public function testUnreadableFileHashesReturnNull(): void
+ {
+ $option = new Option(
+ driver: ScanFileDriver::class,
+ watchPaths: [
+ new WatchPath('/tmp/unreadable.php', WatchPathType::File),
+ ],
+ scanInterval: 1,
+ );
+
+ $logger = ContainerStub::getLogger();
+ $filesystem = m::mock(Filesystem::class);
+ $filesystem->shouldReceive('hash')
+ ->once()
+ ->with('/tmp/unreadable.php')
+ ->andReturn(false);
+
+ $driver = new class($option, $logger, $filesystem) extends ScanFileDriver {
+ public function hashPath(string $path): ?string
+ {
+ return $this->hashFile($path);
+ }
+ };
+
+ $this->assertNull($driver->hashPath('/tmp/unreadable.php'));
+
+ $driver->stop();
+ }
}
diff --git a/tests/Watcher/Fixtures/ScanFileDriverStub.php b/tests/Watcher/Fixtures/ScanFileDriverStub.php
index 3ce0af1c8..2b36ee254 100644
--- a/tests/Watcher/Fixtures/ScanFileDriverStub.php
+++ b/tests/Watcher/Fixtures/ScanFileDriverStub.php
@@ -8,8 +8,8 @@
class ScanFileDriverStub extends ScanFileDriver
{
- protected function getWatchMD5(): array
+ protected function getWatchFileHashes(): array
{
- return ['.env' => md5(strval(microtime()))];
+ return ['.env' => hash('xxh128', strval(microtime()))];
}
}