diff --git a/src/docs/filesystem.md b/src/docs/filesystem.md
index ae0698148..ac0dd29cc 100644
--- a/src/docs/filesystem.md
+++ b/src/docs/filesystem.md
@@ -414,6 +414,8 @@ $disk = Storage::build($configuration, 'tenant-uploads');
Hypervel uses this name as part of the pool identity for drivers that pool the complete filesystem instance. An on-demand disk does not register a serving route of its own. When a named on-demand disk enables `serve`, Hypervel generates signed URLs through the configured served disk with that name. Use matching storage configuration because the route resolves the configured disk, not the on-demand instance. An anonymous scoped disk may instead generate signed URLs through a named parent disk that has serving enabled. S3 and Google Cloud Storage client pools continue to use the client configuration rather than the logical disk name.
+The disk name `ondemand` is reserved and cannot be used in your `filesystems.disks` configuration. In tests, `Storage::fake('ondemand')` or `Storage::persistentFake('ondemand')` replaces disks returned by `Storage::build()` when no logical name is supplied. Builds with an explicit logical name continue to use their own configuration.
+
## Retrieving Files
diff --git a/src/docs/porting-from-laravel.md b/src/docs/porting-from-laravel.md
index 7adeaae1a..3da5ad74f 100644
--- a/src/docs/porting-from-laravel.md
+++ b/src/docs/porting-from-laravel.md
@@ -588,6 +588,8 @@ Hypervel's `Filesystem::hash()` method uses `xxh128` by default. Pass `md5` expl
Unlike Laravel, Hypervel honors `read-only` on scoped disk records. Remove that option from any scoped disk that must accept writes.
+Rename any configured disk called `ondemand`; Hypervel reserves that name for [on-demand disk fakes](/docs/{{version}}/filesystem#on-demand-disks).
+
### Tinker
diff --git a/src/filesystem/README.md b/src/filesystem/README.md
index e7a464849..cdabe2595 100644
--- a/src/filesystem/README.md
+++ b/src/filesystem/README.md
@@ -11,6 +11,8 @@ Hypervel omits Laravel's legacy `Storage::cloud()` / `filesystem.cloud` default-
Hypervel's `Filesystem::hash()` method uses `xxh128` by default instead of `md5`. Pass `md5` explicitly when Laravel-compatible digests are required.
+The configured disk name `ondemand` is reserved. Disks explicitly set under that name, including fakes, intercept anonymous `build()` calls; configured disks with that name are rejected on resolution.
+
Hypervel pools S3 and Google Cloud Storage SDK clients rather than complete disk adapters. Disks with equivalent client construction config share the expensive client pool while retaining their own bucket, root, visibility, and callback behavior. Pooled disks expose raw internals only through borrow-scoped `withClient()`, `withDriver()`, and `withAdapter()` callbacks.
Filesystem construction differs from Laravel in how it carries logical disk identity. `callCustomCreator()` accepts the logical disk name as an optional second parameter, so existing one-argument calls remain valid while overrides must adopt the parameter. The public `build()` method uses a logical-name-aware construction path rather than the protected `resolve()` method because anonymous builds must pass a null name to creators; `resolve()` remains the configured-disk seam. `createScopedDriver()` resolves its prepared descriptor directly rather than through `build()`, so override `createScopedDriver()` for scoped construction customization. Customize on-demand construction through `Storage::extend()` or the public driver creator methods. Creator callbacks may accept the nullable name as a third argument after the application and configuration, while existing two-argument callbacks remain valid. Hypervel carries the name through scoped reconstruction and whole-driver pool fingerprints. A matching explicit fingerprint declares that every construction detail is equivalent, including serving-route ownership.
diff --git a/src/filesystem/src/FilesystemManager.php b/src/filesystem/src/FilesystemManager.php
index 3ae222df0..fc962ee95 100644
--- a/src/filesystem/src/FilesystemManager.php
+++ b/src/filesystem/src/FilesystemManager.php
@@ -137,6 +137,10 @@ public function disk(UnitEnum|string|null $name = null): Filesystem
*/
public function build(array|string $config, ?string $name = null): Filesystem
{
+ if ($name === null && isset($this->disks[self::ON_DEMAND_DISK_NAME])) {
+ return $this->disks[self::ON_DEMAND_DISK_NAME];
+ }
+
$config = is_array($config) ? $config : [
'driver' => 'local',
'root' => $config,
@@ -170,8 +174,8 @@ protected function resolve(string $name, ?array $config = null): Filesystem
/**
* Resolve the given disk while preserving its logical construction name.
*
- * The configured disk name "ondemand" is valid, so build() enters this
- * method directly to carry anonymous construction as a separate value.
+ * Anonymous builds carry a null logical name through custom creators
+ * and pool identity, independently of their internal construction name.
*/
private function resolveWithLogicalName(string $name, ?array $config, ?string $logicalName): Filesystem
{
@@ -834,7 +838,13 @@ public function set(string $name, mixed $disk): static
*/
protected function getConfig(string $name): array
{
- return $this->app->make('config')->get("filesystems.disks.{$name}") ?: [];
+ $config = $this->app->make('config')->get("filesystems.disks.{$name}") ?: [];
+
+ if ($name === self::ON_DEMAND_DISK_NAME && $config !== []) {
+ throw new InvalidArgumentException('The disk name [ondemand] is reserved for on-demand disk fakes. Rename the configured disk.');
+ }
+
+ return $config;
}
/**
diff --git a/src/routing/src/UrlGenerator.php b/src/routing/src/UrlGenerator.php
index a0dedc6e5..4f2a5d334 100755
--- a/src/routing/src/UrlGenerator.php
+++ b/src/routing/src/UrlGenerator.php
@@ -406,7 +406,7 @@ public function hasCorrectSignature(Request $request, bool $absolute = true, Clo
$url = $absolute
? rtrim($request->getSchemeAndHttpHost() . $request->getBaseUrl() . $request->getPathInfo(), '/')
- : '/' . $request->path();
+ : '/' . trim($request->getPathInfo(), '/');
// REMOVED: Vapor's VAPOR_RAW_QUERY_STRING override; Swoole supplies the raw QUERY_STRING.
// Keep this explode() to avoid an extra Stringable allocation per call.
diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php
index 98ec308d2..abae5babe 100644
--- a/tests/Filesystem/FilesystemManagerTest.php
+++ b/tests/Filesystem/FilesystemManagerTest.php
@@ -117,6 +117,20 @@ public function testCanBuildOnDemandDisk(): void
]));
}
+ public function testConfiguredOndemandDiskNameIsReserved(): void
+ {
+ $filesystem = new FilesystemManager($this->getContainer([
+ 'disks' => [
+ 'ondemand' => ['driver' => 'local', 'root' => $this->tempDir],
+ ],
+ ]));
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('The disk name [ondemand] is reserved for on-demand disk fakes. Rename the configured disk.');
+
+ $filesystem->disk('ondemand');
+ }
+
public function testCanBuildReadOnlyDisks(): void
{
$filesystem = new FilesystemManager($this->getContainer());
@@ -1198,9 +1212,7 @@ public function testCustomCreatorsReceiveTheExactLogicalName(): void
'driver' => 'custom',
'root' => $this->tempDir . '/custom-names',
];
- $container = $this->getContainer([
- 'disks' => ['ondemand' => $config],
- ]);
+ $container = $this->getContainer();
$received = [];
$manager = new FilesystemManager($container);
$manager->extend('custom', function (Container $app, array $config, ?string $name) use (&$received): FilesystemAdapter {
@@ -1212,7 +1224,7 @@ public function testCustomCreatorsReceiveTheExactLogicalName(): void
$manager->build($config);
$manager->build($config, 'uploads');
- $manager->disk('ondemand');
+ $manager->build($config, 'ondemand');
$this->assertSame([null, 'uploads', 'ondemand'], $received);
}
@@ -1249,7 +1261,6 @@ public function testCustomPoolableDriversIncludeLogicalNamesAndExcludePoolMetada
'disks' => [
'first' => $config,
'second' => $config,
- 'ondemand' => $config,
],
]);
Container::setInstance($container);
@@ -1264,15 +1275,15 @@ public function testCustomPoolableDriversIncludeLogicalNamesAndExcludePoolMetada
$first = $manager->disk('first');
$second = $manager->disk('second');
$anonymous = $manager->build($config);
- $configuredOndemand = $manager->disk('ondemand');
+ $namedOndemand = $manager->build($config, 'ondemand');
$this->assertInstanceOf(FilesystemPoolProxy::class, $first);
$this->assertNotSame($first->getPoolName(), $second->getPoolName());
- $this->assertNotSame($anonymous->getPoolName(), $configuredOndemand->getPoolName());
+ $this->assertNotSame($anonymous->getPoolName(), $namedOndemand->getPoolName());
$this->assertFalse($first->exists('missing.txt'));
$this->assertFalse($second->exists('missing.txt'));
$this->assertFalse($anonymous->exists('missing.txt'));
- $this->assertFalse($configuredOndemand->exists('missing.txt'));
+ $this->assertFalse($namedOndemand->exists('missing.txt'));
$this->assertSame(['first', 'second', null, 'ondemand'], array_column($received, 1));
foreach ($received as [$receivedConfig]) {
@@ -1292,20 +1303,19 @@ public function testPoolableBuiltInDriversIncludeTheLogicalNameInConstructionIde
'disks' => [
'first' => $config,
'second' => $config,
- 'ondemand' => $config,
],
]);
$manager = (new FilesystemManager($container))->addPoolableDriver('local');
$first = $manager->disk('first');
$second = $manager->disk('second');
- $configuredOndemand = $manager->disk('ondemand');
+ $namedOndemand = $manager->build($config, 'ondemand');
$anonymous = $manager->build($config);
$this->assertInstanceOf(FilesystemPoolProxy::class, $first);
$this->assertInstanceOf(FilesystemPoolProxy::class, $second);
$this->assertNotSame($first->getPoolName(), $second->getPoolName());
- $this->assertNotSame($configuredOndemand->getPoolName(), $anonymous->getPoolName());
+ $this->assertNotSame($namedOndemand->getPoolName(), $anonymous->getPoolName());
}
public function testWholeDriverPoolsIncludeRouteOwnershipNotImpliedByEffectiveConfiguration(): void
diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php
index f3bf3ff29..6d3933e31 100644
--- a/tests/Http/HttpRequestTest.php
+++ b/tests/Http/HttpRequestTest.php
@@ -2257,7 +2257,7 @@ public function testItDoesNotGenerateJsonErrorsForEmptyContent(): void
Request::create('', 'GET')->json();
- $this->assertTrue(json_last_error() === JSON_ERROR_NONE);
+ $this->assertSame(JSON_ERROR_NONE, json_last_error());
}
public function testItClampsValues(): void
diff --git a/tests/Http/JsonResourceTest.php b/tests/Http/JsonResourceTest.php
index 259160159..98d4f297c 100644
--- a/tests/Http/JsonResourceTest.php
+++ b/tests/Http/JsonResourceTest.php
@@ -44,7 +44,7 @@ public function testJsonResourceToJsonSucceedsWithPriorErrors(): void
// Simulate a JSON error
json_decode('{');
- $this->assertTrue(json_last_error() !== JSON_ERROR_NONE);
+ $this->assertNotSame(JSON_ERROR_NONE, json_last_error());
$this->assertSame('{"foo":"bar"}', $resource->toJson(JSON_THROW_ON_ERROR));
}
diff --git a/tests/Integration/Database/EloquentUniqueStringPrimaryKeysTest.php b/tests/Integration/Database/EloquentUniqueStringPrimaryKeysTest.php
index 634b4f5a8..a57d5d1cb 100644
--- a/tests/Integration/Database/EloquentUniqueStringPrimaryKeysTest.php
+++ b/tests/Integration/Database/EloquentUniqueStringPrimaryKeysTest.php
@@ -67,11 +67,11 @@ public function testModelWithUlidPrimaryKeyCanBeCreated()
$this->assertTrue(Str::isUlid($user->bar));
}
- public function testModelWithoutUuidPrimaryKeyCanBeCreated()
+ public function testModelWithoutUuidPrimaryKeyCanBeCreated(): void
{
$user = ModelWithoutUuidPrimaryKey::create();
- $this->assertTrue(is_int($user->id));
+ $this->assertIsInt($user->id);
$this->assertTrue(Str::isUuid($user->foo));
$this->assertTrue(Str::isUuid($user->bar));
}
@@ -105,13 +105,13 @@ public function testModelWithUlidPrimaryKeyCanBeCreatedQuietly()
$this->assertTrue(Str::isUlid($user->bar));
}
- public function testModelWithoutUuidPrimaryKeyCanBeCreatedQuietly()
+ public function testModelWithoutUuidPrimaryKeyCanBeCreatedQuietly(): void
{
$user = new ModelWithoutUuidPrimaryKey;
$user->saveQuietly();
- $this->assertTrue(is_int($user->id));
+ $this->assertIsInt($user->id);
$this->assertTrue(Str::isUuid($user->foo));
$this->assertTrue(Str::isUuid($user->bar));
}
diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php
index e19ab25cb..b2a2571d6 100644
--- a/tests/Integration/Database/SchemaBuilderTest.php
+++ b/tests/Integration/Database/SchemaBuilderTest.php
@@ -392,14 +392,14 @@ public function testGetColumns()
));
}
- public function testGetColumnsOnView()
+ public function testGetColumnsOnView(): void
{
DB::statement('create view foo (bar) as select 1');
$columns = Schema::getColumns('foo');
$this->assertCount(1, $columns);
- $this->assertTrue($columns[0]['name'] === 'bar');
+ $this->assertSame('bar', $columns[0]['name']);
}
public function testGetIndexes()
diff --git a/tests/Integration/Support/StorageFacadeTest.php b/tests/Integration/Support/StorageFacadeTest.php
index a0d163f86..3828c9df9 100644
--- a/tests/Integration/Support/StorageFacadeTest.php
+++ b/tests/Integration/Support/StorageFacadeTest.php
@@ -86,6 +86,43 @@ public function testStorageFakeMethodsWithEnums(): void
$this->assertNull(Storage::fake(StorageFakeStringDisk::Public)->get('nonExistentFile'));
}
+ public function testCanFakeOnDemandDisk(): void
+ {
+ $fake = Storage::fake('ondemand');
+
+ Storage::build([
+ 'driver' => 'ftp',
+ 'host' => 'example.com',
+ 'username' => 'foo',
+ 'password' => 'bar',
+ ])->put('file.txt', 'contents');
+
+ $fake->assertExists('file.txt');
+ }
+
+ public function testOnDemandFakesPreserveNamedBuildsAndCanBeCleared(): void
+ {
+ $root = storage_path('on-demand');
+ $config = ['driver' => 'local', 'root' => $root];
+ $fake = Storage::persistentFake('ondemand');
+
+ $this->assertSame($fake, Storage::build($config));
+ $this->assertNotSame($fake, Storage::build($config, 'uploads'));
+
+ Storage::forgetDisk('ondemand');
+ Storage::build($config)->put('after-forget.txt', 'contents');
+ $this->assertFileExists($root . '/after-forget.txt');
+ $fake->assertMissing('after-forget.txt');
+
+ $fake = Storage::fake('ondemand');
+ $this->assertSame($fake, Storage::build($root));
+
+ Storage::purge('ondemand');
+ Storage::build($root)->put('after-purge.txt', 'contents');
+ $this->assertFileExists($root . '/after-purge.txt');
+ $fake->assertMissing('after-purge.txt');
+ }
+
public function testFakePreservesOriginalDiskThrowConfig(): void
{
config(['filesystems.disks.local.throw' => true]);
diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php
index 577df8f36..08962c463 100644
--- a/tests/Process/ProcessTest.php
+++ b/tests/Process/ProcessTest.php
@@ -297,8 +297,8 @@ public function testProcessPoolCanReceiveOutputForEachProcessViaStartMethod(): v
$poolResults = $pool->wait();
- $this->assertTrue($output[0]['out'] !== []);
- $this->assertTrue($output[1]['out'] !== []);
+ $this->assertNotSame([], $output[0]['out']);
+ $this->assertNotSame([], $output[1]['out']);
$this->assertInstanceOf(ProcessResult::class, $poolResults[0]);
$this->assertInstanceOf(ProcessResult::class, $poolResults[1]);
$this->assertStringContainsString('ProcessTest.php', $poolResults[0]->output());
diff --git a/tests/Routing/RoutingUrlGeneratorTest.php b/tests/Routing/RoutingUrlGeneratorTest.php
index 2064f0742..918b2d384 100755
--- a/tests/Routing/RoutingUrlGeneratorTest.php
+++ b/tests/Routing/RoutingUrlGeneratorTest.php
@@ -1150,17 +1150,17 @@ public function testSignedUrlImplicitModelBinding()
$this->assertTrue($url->hasValidSignature($request));
}
- public function testSignedRelativeUrl()
+ public function testSignedRelativeUrl(): void
{
$url = new UrlGenerator(
$routes = new RouteCollection,
$request = Request::create('http://www.foo.com/')
);
- $url->setKeyResolver(function () {
+ $url->setKeyResolver(function (): string {
return 'secret';
});
- $route = new Route(['GET'], 'foo', ['as' => 'foo', function () {
+ $route = new Route(['GET'], 'foo', ['as' => 'foo', function (): void {
}]);
$routes->add($route);
@@ -1173,6 +1173,16 @@ public function testSignedRelativeUrl()
$request = Request::create($url->signedRoute('foo', [], null, false) . '?tampered=true');
$this->assertFalse($url->hasValidSignature($request, false));
+
+ $routes->add(new Route(['GET'], '/', ['as' => 'home']));
+
+ $request = Request::create($url->signedRoute('home', [], null, false));
+
+ $this->assertTrue($url->hasValidRelativeSignature($request));
+
+ $request->query->set('signature', 'invalid');
+
+ $this->assertFalse($url->hasValidRelativeSignature($request));
}
public function testSignedUrlParameterCannotBeNamedSignature(): void
diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php
index 8413a866f..27c10de9a 100644
--- a/tests/Support/SupportStrTest.php
+++ b/tests/Support/SupportStrTest.php
@@ -2283,7 +2283,7 @@ public function testResetFactoryState(): void
public function testPasswordCreation(): void
{
- $this->assertTrue(strlen(Str::password()) === 32);
+ $this->assertSame(32, strlen(Str::password()));
$this->assertStringNotContainsString(' ', Str::password());
$this->assertStringContainsString(' ', Str::password(spaces: true));