From add16f01d8d965ed9eb730f68cd38a78b462ceec Mon Sep 17 00:00:00 2001
From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com>
Date: Wed, 9 Sep 2026 16:44:14 +0000
Subject: [PATCH 01/23] Complete email verification and having-range
documentation
Document resetting an email verification status after an address change,
and describe the not-between and OR variants alongside havingBetween.
These APIs and the later MustVerifyEmail contract addition already match
the current Laravel source; no runtime changes are necessary.
Port both upstream human-readable comment corrections in the console
traits. Preserve Hypervel's native property types and existing behavior.
Upstream:
https://github.com/laravel/framework/pull/58255
https://github.com/laravel/framework/pull/58701
https://github.com/laravel/framework/pull/58259
https://github.com/laravel/framework/pull/58266
Compared complete PR diffs with Laravel 13.x at
01d008c9b5f32cb7c5e50a9a22273113d810b2a2. Checked Laravel docs first;
the public usage explanations were absent. Formatting and diff checks
pass. No runtime tests or static analysis needed for prose and comments.
---
src/console/src/Concerns/InteractsWithIO.php | 2 +-
src/console/src/Scheduling/ManagesAttributes.php | 2 +-
src/docs/queries.md | 2 ++
src/docs/verification.md | 6 ++++++
4 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/console/src/Concerns/InteractsWithIO.php b/src/console/src/Concerns/InteractsWithIO.php
index c6a45d30eb..d293efbcb5 100644
--- a/src/console/src/Concerns/InteractsWithIO.php
+++ b/src/console/src/Concerns/InteractsWithIO.php
@@ -35,7 +35,7 @@ trait InteractsWithIO
protected int $verbosity = OutputInterface::VERBOSITY_NORMAL;
/**
- * The mapping between human readable verbosity levels and Symfony's OutputInterface.
+ * The mapping between human-readable verbosity levels and Symfony's OutputInterface.
*/
protected array $verbosityMap = [
'v' => OutputInterface::VERBOSITY_VERBOSE,
diff --git a/src/console/src/Scheduling/ManagesAttributes.php b/src/console/src/Scheduling/ManagesAttributes.php
index 4a68e1b3fa..7da4bceba1 100644
--- a/src/console/src/Scheduling/ManagesAttributes.php
+++ b/src/console/src/Scheduling/ManagesAttributes.php
@@ -80,7 +80,7 @@ trait ManagesAttributes
protected array $rejects = [];
/**
- * The human readable description of the event.
+ * The human-readable description of the event.
*/
public ?string $description = null;
diff --git a/src/docs/queries.md b/src/docs/queries.md
index d286f1b2ba..0498ba97ad 100644
--- a/src/docs/queries.md
+++ b/src/docs/queries.md
@@ -1451,6 +1451,8 @@ $report = DB::table('orders')
->get();
```
+The `havingNotBetween` method excludes results within the given range. You may use `orHavingBetween` and `orHavingNotBetween` to join these conditions to the previous having clause using `or`.
+
You may pass multiple arguments to the `groupBy` method to group by multiple columns:
```php
diff --git a/src/docs/verification.md b/src/docs/verification.md
index cd4107f6f9..6b7dd0605d 100644
--- a/src/docs/verification.md
+++ b/src/docs/verification.md
@@ -100,6 +100,12 @@ Before moving on, let's take a closer look at this route. First, you'll notice w
Next, we can proceed directly to calling the `fulfill` method on the request. This method will call the `markEmailAsVerified` method on the authenticated user and dispatch the `Hypervel\Auth\Events\Verified` event. The `markEmailAsVerified` method is available to the default `App\Models\User` model via the `Hypervel\Foundation\Auth\User` base class. Once the user's email address has been verified, you may redirect them wherever you wish.
+You may reset a user's email verification status using the `markEmailAsUnverified` method, for example, after the user changes their email address. This clears the stored verification timestamp and saves the user:
+
+```php
+$user->markEmailAsUnverified();
+```
+
By default, verification links expire after 60 minutes. You may change this duration using the `auth.verification.expire` configuration option.
From a7272dbab0cca82083c2155fc4f2aa91778c0dbe Mon Sep 17 00:00:00 2001
From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com>
Date: Wed, 9 Sep 2026 17:54:35 +0000
Subject: [PATCH 02/23] Complete encryption and collection porting conventions
Reconcile Laravel framework PRs #58262, #58283 and #58289 against 13.x
at 01d008c9b5f32cb7c5e50a9a22273113d810b2a2. Readable environment-file
encryption, format detection and all applicable upstream tests are already
present, as are eager/lazy collection argument guards and exception docs.
Complete the remaining Hypervel adaptations: natively type encryption
tests and their tampered-payload provider, use Hypervel fixture names in
both environment commands' tests, link the encryption package's existing
documentation and upstream, and restore the inherited split description.
Preserve atomic file replacement, permissions, cancellation behavior,
existing regression assertions, collection generic metadata and laziness.
This changes no production behavior and adds no tests.
All three affected test files pass. Scoped PHP-CS-Fixer and git diff
checks pass; production changes are documentation only.
https://github.com/laravel/framework/pull/58262
https://github.com/laravel/framework/pull/58283
https://github.com/laravel/framework/pull/58289
---
src/collections/src/LazyCollection.php | 2 +
src/encryption/README.md | 6 +-
tests/Encryption/EncrypterTest.php | 57 ++++++++++---------
.../Console/EnvironmentDecryptCommandTest.php | 44 +++++++-------
.../Console/EnvironmentEncryptCommandTest.php | 6 +-
5 files changed, 62 insertions(+), 53 deletions(-)
diff --git a/src/collections/src/LazyCollection.php b/src/collections/src/LazyCollection.php
index 5ea4f4371e..07186de265 100644
--- a/src/collections/src/LazyCollection.php
+++ b/src/collections/src/LazyCollection.php
@@ -1225,6 +1225,8 @@ public function slice(int $offset, ?int $length = null): static
}
/**
+ * Split a collection into a certain number of groups.
+ *
* @throws InvalidArgumentException
*/
#[Override]
diff --git a/src/encryption/README.md b/src/encryption/README.md
index c22d0c8247..9eef4c8375 100644
--- a/src/encryption/README.md
+++ b/src/encryption/README.md
@@ -1,4 +1,8 @@
Encryption for Hypervel
===
-[](https://deepwiki.com/hypervel/encryption)
\ No newline at end of file
+[](https://deepwiki.com/hypervel/encryption)
+
+Documentation: https://hypervel.org/docs/encryption
+
+Ported from: https://github.com/laravel/framework/tree/13.x/src/Illuminate/Encryption
diff --git a/tests/Encryption/EncrypterTest.php b/tests/Encryption/EncrypterTest.php
index 71c4d8ecef..6dbcc16dcb 100644
--- a/tests/Encryption/EncrypterTest.php
+++ b/tests/Encryption/EncrypterTest.php
@@ -13,7 +13,7 @@
class EncrypterTest extends TestCase
{
- public function testEncryption()
+ public function testEncryption(): void
{
$e = new Encrypter(str_repeat('a', 16));
$encrypted = $e->encrypt('foo');
@@ -33,7 +33,7 @@ public function testEncryption()
$this->assertSame($data, $e->decrypt($encryptedArray));
}
- public function testRawStringEncryption()
+ public function testRawStringEncryption(): void
{
$e = new Encrypter(str_repeat('a', 16));
$encrypted = $e->encryptString('foo');
@@ -41,7 +41,7 @@ public function testRawStringEncryption()
$this->assertSame('foo', $e->decryptString($encrypted));
}
- public function testRawStringEncryptionWithPreviousKeys()
+ public function testRawStringEncryptionWithPreviousKeys(): void
{
$previous = new Encrypter(str_repeat('b', 16));
$previousValue = $previous->encryptString('foo');
@@ -53,7 +53,7 @@ public function testRawStringEncryptionWithPreviousKeys()
$this->assertSame('foo', $decrypted);
}
- public function testItValidatesMacOnPerKeyBasis()
+ public function testItValidatesMacOnPerKeyBasis(): void
{
// Payload created with (key: str_repeat('b', 16)) but will
// "successfully" decrypt with (key: str_repeat('a', 16)), however it
@@ -82,7 +82,7 @@ public function testItValidatesEveryMacBeforeDecryptingWithTheFirstValidKey(): v
);
}
- public function testEncryptionUsingBase64EncodedKey()
+ public function testEncryptionUsingBase64EncodedKey(): void
{
$e = new Encrypter(random_bytes(16));
$encrypted = $e->encrypt('foo');
@@ -90,7 +90,7 @@ public function testEncryptionUsingBase64EncodedKey()
$this->assertSame('foo', $e->decrypt($encrypted));
}
- public function testEncryptedLengthIsFixed()
+ public function testEncryptedLengthIsFixed(): void
{
$e = new Encrypter(str_repeat('a', 16));
$lengths = [];
@@ -100,7 +100,7 @@ public function testEncryptedLengthIsFixed()
$this->assertSame(min($lengths), max($lengths));
}
- public function testWithCustomCipher()
+ public function testWithCustomCipher(): void
{
$e = new Encrypter(str_repeat('b', 32), 'AES-256-GCM');
$encrypted = $e->encrypt('bar');
@@ -113,7 +113,7 @@ public function testWithCustomCipher()
$this->assertSame('foo', $e->decrypt($encrypted));
}
- public function testCipherNamesCanBeMixedCase()
+ public function testCipherNamesCanBeMixedCase(): void
{
$upper = new Encrypter(str_repeat('b', 16), 'AES-128-GCM');
$encrypted = $upper->encrypt('bar');
@@ -126,7 +126,7 @@ public function testCipherNamesCanBeMixedCase()
$this->assertSame('bar', $mixed->decrypt($encrypted));
}
- public function testThatAnAeadCipherIncludesTag()
+ public function testThatAnAeadCipherIncludesTag(): void
{
$e = new Encrypter(str_repeat('b', 32), 'AES-256-GCM');
$encrypted = $e->encrypt('foo');
@@ -136,7 +136,7 @@ public function testThatAnAeadCipherIncludesTag()
$this->assertNotEmpty($data->tag);
}
- public function testThatAnAeadTagMustBeProvidedInFullLength()
+ public function testThatAnAeadTagMustBeProvidedInFullLength(): void
{
$e = new Encrypter(str_repeat('b', 32), 'AES-256-GCM');
$encrypted = $e->encrypt('foo');
@@ -174,7 +174,7 @@ public function testThatAnAeadTagMustNotBeEmpty(): void
$encrypter->decrypt(base64_encode(json_encode($payload)));
}
- public function testThatAnAeadTagCantBeModified()
+ public function testThatAnAeadTagCantBeModified(): void
{
$e = new Encrypter(str_repeat('b', 32), 'AES-256-GCM');
$encrypted = $e->encrypt('foo');
@@ -188,7 +188,7 @@ public function testThatAnAeadTagCantBeModified()
$e->decrypt($encrypted);
}
- public function testThatANonAeadCipherIncludesMac()
+ public function testThatANonAeadCipherIncludesMac(): void
{
$e = new Encrypter(str_repeat('b', 32), 'AES-256-CBC');
$encrypted = $e->encrypt('foo');
@@ -198,7 +198,7 @@ public function testThatANonAeadCipherIncludesMac()
$this->assertNotEmpty($data->mac);
}
- public function testDoNoAllowLongerKey()
+ public function testDoNoAllowLongerKey(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Unsupported cipher or incorrect key length. Supported ciphers are: aes-128-cbc, aes-256-cbc, aes-128-gcm, aes-256-gcm.');
@@ -206,7 +206,7 @@ public function testDoNoAllowLongerKey()
new Encrypter(str_repeat('z', 32));
}
- public function testWithBadKeyLength()
+ public function testWithBadKeyLength(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Unsupported cipher or incorrect key length. Supported ciphers are: aes-128-cbc, aes-256-cbc, aes-128-gcm, aes-256-gcm.');
@@ -214,7 +214,7 @@ public function testWithBadKeyLength()
new Encrypter(str_repeat('a', 5));
}
- public function testWithBadKeyLengthAlternativeCipher()
+ public function testWithBadKeyLengthAlternativeCipher(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Unsupported cipher or incorrect key length. Supported ciphers are: aes-128-cbc, aes-256-cbc, aes-128-gcm, aes-256-gcm.');
@@ -222,7 +222,7 @@ public function testWithBadKeyLengthAlternativeCipher()
new Encrypter(str_repeat('a', 16), 'AES-256-GCM');
}
- public function testWithUnsupportedCipher()
+ public function testWithUnsupportedCipher(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Unsupported cipher or incorrect key length. Supported ciphers are: aes-128-cbc, aes-256-cbc, aes-128-gcm, aes-256-gcm.');
@@ -230,7 +230,7 @@ public function testWithUnsupportedCipher()
new Encrypter(str_repeat('c', 16), 'AES-256-CFB8');
}
- public function testExceptionThrownWhenPayloadIsInvalid()
+ public function testExceptionThrownWhenPayloadIsInvalid(): void
{
$this->expectException(DecryptException::class);
$this->expectExceptionMessage('The payload is invalid.');
@@ -241,7 +241,7 @@ public function testExceptionThrownWhenPayloadIsInvalid()
$e->decrypt($payload);
}
- public function testDecryptionExceptionIsThrownWhenUnexpectedTagIsAdded()
+ public function testDecryptionExceptionIsThrownWhenUnexpectedTagIsAdded(): void
{
$this->expectException(DecryptException::class);
$this->expectExceptionMessage('Unable to use tag because the cipher algorithm does not support AEAD.');
@@ -253,7 +253,7 @@ public function testDecryptionExceptionIsThrownWhenUnexpectedTagIsAdded()
$e->decrypt(base64_encode(json_encode($decodedPayload)));
}
- public function testExceptionThrownWithDifferentKey()
+ public function testExceptionThrownWithDifferentKey(): void
{
$this->expectException(DecryptException::class);
$this->expectExceptionMessage('The MAC is invalid.');
@@ -263,7 +263,7 @@ public function testExceptionThrownWithDifferentKey()
$b->decrypt($a->encrypt('baz'));
}
- public function testExceptionThrownWhenIvIsTooLong()
+ public function testExceptionThrownWhenIvIsTooLong(): void
{
$this->expectException(DecryptException::class);
$this->expectExceptionMessage('The payload is invalid.');
@@ -277,7 +277,7 @@ public function testExceptionThrownWhenIvIsTooLong()
$e->decrypt($modified_payload);
}
- public function testSupportedMethodAcceptsAnyCasing()
+ public function testSupportedMethodAcceptsAnyCasing(): void
{
$key = str_repeat('a', 16);
@@ -287,7 +287,7 @@ public function testSupportedMethodAcceptsAnyCasing()
}
#[DataProvider('provideTamperedData')]
- public function testTamperedPayloadWillGetRejected($payload)
+ public function testTamperedPayloadWillGetRejected(array $payload): void
{
$this->expectException(DecryptException::class);
$this->expectExceptionMessage('The payload is invalid.');
@@ -296,7 +296,10 @@ public function testTamperedPayloadWillGetRejected($payload)
$enc->decrypt(base64_encode(json_encode($payload)));
}
- public static function provideTamperedData()
+ /**
+ * Provide tampered encrypted payloads.
+ */
+ public static function provideTamperedData(): array
{
$validIv = base64_encode(str_repeat('.', 16));
@@ -312,7 +315,7 @@ public static function provideTamperedData()
];
}
- public function testEncryptedReturnsTrueForEncryptedValue()
+ public function testEncryptedReturnsTrueForEncryptedValue(): void
{
$e = new Encrypter(str_repeat('a', 16));
$encrypted = $e->encrypt('foo');
@@ -320,7 +323,7 @@ public function testEncryptedReturnsTrueForEncryptedValue()
$this->assertTrue(Encrypter::appearsEncrypted($encrypted));
}
- public function testEncryptedReturnsTrueForEncryptedArray()
+ public function testEncryptedReturnsTrueForEncryptedArray(): void
{
$e = new Encrypter(str_repeat('a', 16));
$encrypted = $e->encrypt(['foo' => 'bar']);
@@ -328,14 +331,14 @@ public function testEncryptedReturnsTrueForEncryptedArray()
$this->assertTrue(Encrypter::appearsEncrypted($encrypted));
}
- public function testEncryptedReturnsFalseForPlainText()
+ public function testEncryptedReturnsFalseForPlainText(): void
{
$this->assertFalse(Encrypter::appearsEncrypted('foo'));
$this->assertFalse(Encrypter::appearsEncrypted('APP_NAME=Hypervel'));
$this->assertFalse(Encrypter::appearsEncrypted("APP_NAME=Hypervel\nAPP_ENV=local"));
}
- public function testEncryptedReturnsFalseForNonString()
+ public function testEncryptedReturnsFalseForNonString(): void
{
$this->assertFalse(Encrypter::appearsEncrypted(123));
$this->assertFalse(Encrypter::appearsEncrypted(['foo' => 'bar']));
diff --git a/tests/Integration/Console/EnvironmentDecryptCommandTest.php b/tests/Integration/Console/EnvironmentDecryptCommandTest.php
index 34a02f6247..aaaa719ab1 100644
--- a/tests/Integration/Console/EnvironmentDecryptCommandTest.php
+++ b/tests/Integration/Console/EnvironmentDecryptCommandTest.php
@@ -107,7 +107,7 @@ public function testItGeneratesTheEnvironmentFileWithGeneratedKey(): void
->once()
->andReturn(
(new Encrypter($key = Encrypter::generateKey('AES-256-CBC'), 'AES-256-CBC'))
- ->encrypt('APP_NAME=Laravel')
+ ->encrypt('APP_NAME=Hypervel')
);
$this->artisan('env:decrypt', ['--force' => true, '--key' => 'base64:' . base64_encode($key)])
@@ -115,7 +115,7 @@ public function testItGeneratesTheEnvironmentFileWithGeneratedKey(): void
->assertExitCode(0);
$this->filesystem->shouldHaveReceived('replace')
- ->with(base_path('.env'), 'APP_NAME=Laravel', 0640);
+ ->with(base_path('.env'), 'APP_NAME=Hypervel', 0640);
}
public function testItGeneratesTheEnvironmentFileWithUserProvidedKey(): void
@@ -130,7 +130,7 @@ public function testItGeneratesTheEnvironmentFileWithUserProvidedKey(): void
->once()
->andReturn(
(new Encrypter('abcdefghijklmnop', 'aes-128-gcm'))
- ->encrypt('APP_NAME="Laravel Two"')
+ ->encrypt('APP_NAME="Hypervel Two"')
);
$this->artisan('env:decrypt', ['--cipher' => 'aes-128-gcm', '--key' => 'abcdefghijklmnop'])
@@ -138,7 +138,7 @@ public function testItGeneratesTheEnvironmentFileWithUserProvidedKey(): void
->assertExitCode(0);
$this->filesystem->shouldHaveReceived('replace')
- ->with(base_path('.env'), 'APP_NAME="Laravel Two"', 0600);
+ ->with(base_path('.env'), 'APP_NAME="Hypervel Two"', 0600);
}
public function testItGeneratesTheEnvironmentFileWithKeyFromEnvironment(): void
@@ -158,7 +158,7 @@ public function testItGeneratesTheEnvironmentFileWithKeyFromEnvironment(): void
->once()
->andReturn(
(new Encrypter('ponmlkjihgfedcbaponmlkjihgfedcba', 'AES-256-CBC'))
- ->encrypt('APP_NAME="Laravel Three"')
+ ->encrypt('APP_NAME="Hypervel Three"')
);
$this->artisan('env:decrypt')
@@ -166,7 +166,7 @@ public function testItGeneratesTheEnvironmentFileWithKeyFromEnvironment(): void
->assertExitCode(0);
$this->filesystem->shouldHaveReceived('replace')
- ->with(base_path('.env'), 'APP_NAME="Laravel Three"', 0600);
+ ->with(base_path('.env'), 'APP_NAME="Hypervel Three"', 0600);
} finally {
if ($hadEncryptionKey) {
$_SERVER['HYPERVEL_ENV_ENCRYPTION_KEY'] = $previousEncryptionKey;
@@ -188,7 +188,7 @@ public function testItGeneratesTheEnvironmentFileWhenForcing(): void
->once()
->andReturn(
(new Encrypter('abcdefghijklmnop', 'aes-128-gcm'))
- ->encrypt('APP_NAME="Laravel Two"')
+ ->encrypt('APP_NAME="Hypervel Two"')
);
$this->artisan('env:decrypt', ['--force' => true, '--key' => 'abcdefghijklmnop', '--cipher' => 'aes-128-gcm'])
@@ -196,13 +196,13 @@ public function testItGeneratesTheEnvironmentFileWhenForcing(): void
->assertExitCode(0);
$this->filesystem->shouldHaveReceived('replace')
- ->with(base_path('.env'), 'APP_NAME="Laravel Two"', 0640);
+ ->with(base_path('.env'), 'APP_NAME="Hypervel Two"', 0640);
}
public function testItDecryptsMultiLineEnvironmentCorrectly(): void
{
$contents = <<<'Text'
- APP_NAME=Laravel
+ APP_NAME=Hypervel
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost
@@ -214,7 +214,7 @@ public function testItDecryptsMultiLineEnvironmentCorrectly(): void
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
- DB_DATABASE=laravel
+ DB_DATABASE=hypervel
DB_USERNAME=root
DB_PASSWORD=
Text;
@@ -252,7 +252,7 @@ public function testItWritesTheEnvironmentFileCustomFilename(): void
->once()
->andReturn(
(new Encrypter('abcdefghijklmnopabcdefghijklmnop', 'AES-256-CBC'))
- ->encrypt('APP_NAME="Laravel Two"')
+ ->encrypt('APP_NAME="Hypervel Two"')
);
$this->artisan('env:decrypt', ['--env' => 'production', '--key' => 'abcdefghijklmnopabcdefghijklmnop', '--filename' => '.env'])
@@ -260,7 +260,7 @@ public function testItWritesTheEnvironmentFileCustomFilename(): void
->assertExitCode(0);
$this->filesystem->shouldHaveReceived('replace')
- ->with(base_path('.env'), 'APP_NAME="Laravel Two"', 0600);
+ ->with(base_path('.env'), 'APP_NAME="Hypervel Two"', 0600);
}
public function testItWritesTheEnvironmentFileCustomPath(): void
@@ -275,7 +275,7 @@ public function testItWritesTheEnvironmentFileCustomPath(): void
->once()
->andReturn(
(new Encrypter('abcdefghijklmnopabcdefghijklmnop', 'AES-256-CBC'))
- ->encrypt('APP_NAME="Laravel Two"')
+ ->encrypt('APP_NAME="Hypervel Two"')
);
$this->artisan('env:decrypt', ['--env' => 'production', '--key' => 'abcdefghijklmnopabcdefghijklmnop', '--path' => '/tmp'])
@@ -283,7 +283,7 @@ public function testItWritesTheEnvironmentFileCustomPath(): void
->assertExitCode(0);
$this->filesystem->shouldHaveReceived('replace')
- ->with('/tmp' . DIRECTORY_SEPARATOR . '.env.production', 'APP_NAME="Laravel Two"', 0600);
+ ->with('/tmp' . DIRECTORY_SEPARATOR . '.env.production', 'APP_NAME="Hypervel Two"', 0600);
}
public function testItWritesTheEnvironmentFileCustomPathAndFilename(): void
@@ -298,7 +298,7 @@ public function testItWritesTheEnvironmentFileCustomPathAndFilename(): void
->once()
->andReturn(
(new Encrypter('abcdefghijklmnopabcdefghijklmnop', 'AES-256-CBC'))
- ->encrypt('APP_NAME="Laravel Two"')
+ ->encrypt('APP_NAME="Hypervel Two"')
);
$this->artisan('env:decrypt', ['--env' => 'production', '--key' => 'abcdefghijklmnopabcdefghijklmnop', '--filename' => '.env', '--path' => '/tmp'])
@@ -306,7 +306,7 @@ public function testItWritesTheEnvironmentFileCustomPathAndFilename(): void
->assertExitCode(0);
$this->filesystem->shouldHaveReceived('replace')
- ->with('/tmp' . DIRECTORY_SEPARATOR . '.env', 'APP_NAME="Laravel Two"', 0600);
+ ->with('/tmp' . DIRECTORY_SEPARATOR . '.env', 'APP_NAME="Hypervel Two"', 0600);
}
public function testItCannotOverwriteEncryptedFiles(): void
@@ -332,7 +332,7 @@ public function testItGeneratesTheEnvironmentFileWithInteractivelyUserProvidedKe
->once()
->andReturn(
(new Encrypter($key = 'abcdefghijklmnop', 'aes-128-gcm'))
- ->encrypt('APP_NAME="Laravel Two"')
+ ->encrypt('APP_NAME="Hypervel Two"')
);
$this->artisan('env:decrypt', ['--cipher' => 'aes-128-gcm'])
@@ -341,7 +341,7 @@ public function testItGeneratesTheEnvironmentFileWithInteractivelyUserProvidedKe
->assertExitCode(0);
$this->filesystem->shouldHaveReceived('replace')
- ->with(base_path('.env'), 'APP_NAME="Laravel Two"', 0600);
+ ->with(base_path('.env'), 'APP_NAME="Hypervel Two"', 0600);
}
public function testItAutoDetectsAndDecryptsReadableFormat(): void
@@ -350,7 +350,7 @@ public function testItAutoDetectsAndDecryptsReadableFormat(): void
$encrypter = new Encrypter($key, 'AES-256-CBC');
// Create readable format encrypted content
- $encryptedContent = 'APP_NAME=' . $encrypter->encryptString('Laravel') . "\n"
+ $encryptedContent = 'APP_NAME=' . $encrypter->encryptString('Hypervel') . "\n"
. 'APP_ENV=' . $encrypter->encryptString('local');
$this->filesystem->shouldReceive('exists')
@@ -368,7 +368,7 @@ public function testItAutoDetectsAndDecryptsReadableFormat(): void
->assertExitCode(0);
$this->filesystem->shouldHaveReceived('replace')
- ->with(base_path('.env'), "APP_NAME=Laravel\nAPP_ENV=local\n", 0600);
+ ->with(base_path('.env'), "APP_NAME=Hypervel\nAPP_ENV=local\n", 0600);
}
public function testItStillDecryptsBlobFormat(): void
@@ -377,7 +377,7 @@ public function testItStillDecryptsBlobFormat(): void
$encrypter = new Encrypter($key, 'AES-256-CBC');
// Create blob format (entire file encrypted as one)
- $originalContent = "APP_NAME=Laravel\nAPP_ENV=local";
+ $originalContent = "APP_NAME=Hypervel\nAPP_ENV=local";
$encryptedContent = $encrypter->encrypt($originalContent);
$this->filesystem->shouldReceive('exists')
@@ -404,7 +404,7 @@ public function testItDecryptsBlobFormatWithNewlineInContent(): void
$encrypter = new Encrypter($key, 'AES-256-CBC');
// Create blob format and inject a newline (simulating wrapped base64)
- $originalContent = "APP_NAME=Laravel\nAPP_ENV=local";
+ $originalContent = "APP_NAME=Hypervel\nAPP_ENV=local";
$encryptedContent = $encrypter->encrypt($originalContent);
// Insert a newline in the middle of the base64 string
diff --git a/tests/Integration/Console/EnvironmentEncryptCommandTest.php b/tests/Integration/Console/EnvironmentEncryptCommandTest.php
index 995d90be1c..eda3c53152 100644
--- a/tests/Integration/Console/EnvironmentEncryptCommandTest.php
+++ b/tests/Integration/Console/EnvironmentEncryptCommandTest.php
@@ -23,7 +23,7 @@ protected function setUp(): void
$this->filesystem = m::spy(Filesystem::class);
$this->filesystem->shouldReceive('get')
- ->andReturn('APP_NAME=Laravel');
+ ->andReturn('APP_NAME=Hypervel');
$this->filesystem->shouldReceive('replace');
$this->filesystem->shouldReceive('chmod')->andReturn('0640');
File::swap($this->filesystem);
@@ -208,7 +208,7 @@ public function testItEncryptsInReadableFormat(): void
$filesystem->shouldReceive('get')
->with(base_path('.env'))
->once()
- ->andReturn("APP_NAME=Laravel\nAPP_ENV=local");
+ ->andReturn("APP_NAME=Hypervel\nAPP_ENV=local");
$filesystem->shouldReceive('replace')
->once()
->with(base_path('.env.encrypted'), m::on(function ($content) {
@@ -239,7 +239,7 @@ public function testItSkipsCommentsAndBlankLinesInReadableFormat(): void
$filesystem->shouldReceive('get')
->with(base_path('.env'))
->once()
- ->andReturn("# Comment\nAPP_NAME=Laravel\n\nAPP_ENV=local");
+ ->andReturn("# Comment\nAPP_NAME=Hypervel\n\nAPP_ENV=local");
$filesystem->shouldReceive('replace')
->once()
->with(base_path('.env.encrypted'), m::on(function ($content) {
From fae6b7521dd380592e8fb87bf78085dc2cde8797 Mon Sep 17 00:00:00 2001
From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com>
Date: Wed, 9 Sep 2026 18:16:00 +0000
Subject: [PATCH 03/23] Complete binary-cast annotations and filesystem fake
test parity
Restore all seven current upstream filesystem fake tests covering missing
files, inherited throw settings, explicit overrides, and string-backed enum
disks. Merge them into the existing StorageFakeTest so integer-zero enum,
parallel-token, and temporary-URL coverage remain alongside the upstream
cases. Use Hypervel's config helper, typed methods, and configured camelCase
PHPUnit method names.
Complete the current AsBinary and BinaryCodec exception annotations and
required method/provider titles. Narrow the null/blank test parameters to
nullable strings. Preserve Symfony UUID conversion, exact binary identifier
recognition, reusable PDO streams, worker-state cleanup, and list-shaped
format names. No production executable code changes.
Add the filesystem documentation link and put its upstream reference after
the existing differences section without changing those explanations.
Laravel PRs:
https://github.com/laravel/framework/pull/58254
https://github.com/laravel/framework/pull/58287
https://github.com/laravel/framework/pull/53779
Port source: Laravel 13.x at 01d008c9b5f32cb7c5e50a9a22273113d810b2a2.
The binary exception annotations incorporate the relevant current changes
from #59016; its broader surface remains to be reconciled separately.
Validation: all three changed test files pass; scoped PHP CS Fixer and
whitespace checks pass. The filesystem tests exercise real local-adapter
failures and overrides rather than only inspecting configuration.
---
src/database/src/Eloquent/Casts/AsBinary.php | 11 +++
src/filesystem/README.md | 4 +-
src/support/src/BinaryCodec.php | 4 ++
.../DatabaseEloquentAsBinaryCastTest.php | 3 +
.../Filesystem/StorageFakeTest.php | 71 +++++++++++++++++--
tests/Support/SupportBinaryCodecTest.php | 10 ++-
6 files changed, 94 insertions(+), 9 deletions(-)
diff --git a/src/database/src/Eloquent/Casts/AsBinary.php b/src/database/src/Eloquent/Casts/AsBinary.php
index 3b22c2defa..98a44e9cd8 100644
--- a/src/database/src/Eloquent/Casts/AsBinary.php
+++ b/src/database/src/Eloquent/Casts/AsBinary.php
@@ -15,12 +15,17 @@ class AsBinary implements Castable
* Get the caster class to use when casting from / to this cast target.
*
* @param array{string} $arguments
+ *
+ * @throws InvalidArgumentException
*/
public static function castUsing(array $arguments): CastsAttributes
{
return new class($arguments) implements CastsAttributes {
protected string $format;
+ /**
+ * Create a new binary cast instance.
+ */
public function __construct(protected array $arguments)
{
$this->format = $this->arguments[0]
@@ -35,6 +40,9 @@ public function __construct(protected array $arguments)
}
}
+ /**
+ * Transform the attribute from the underlying model values.
+ */
public function get(mixed $model, string $key, mixed $value, array $attributes): ?string
{
$attribute = $attributes[$key] ?? null;
@@ -50,6 +58,9 @@ public function get(mixed $model, string $key, mixed $value, array $attributes):
return BinaryCodec::decode($attribute, $this->format);
}
+ /**
+ * Transform the attribute to its underlying model values.
+ */
public function set(mixed $model, string $key, mixed $value, array $attributes): array
{
return [$key => BinaryCodec::encode($value, $this->format)];
diff --git a/src/filesystem/README.md b/src/filesystem/README.md
index c2acd761a9..e7a4648495 100644
--- a/src/filesystem/README.md
+++ b/src/filesystem/README.md
@@ -3,7 +3,7 @@ Filesystem for Hypervel
[](https://deepwiki.com/hypervel/filesystem)
-Ported from: https://github.com/laravel/framework (illuminate/filesystem)
+Documentation: https://hypervel.org/docs/filesystem
## Differences From Laravel
@@ -18,3 +18,5 @@ Filesystem construction differs from Laravel in how it carries logical disk iden
Hypervel registers signed file-serving routes for any configured disk whose `serve` option is exactly `true`, while Laravel limits these routes to local disks and accepts truthy values. Every served disk must use a unique URL or application boot will fail. Custom drivers that opt in must provide the filesystem response methods used by these routes.
Hypervel also provides `ScopedFilesystemProxy` and `ScopedCloudFilesystemProxy` for prefixes resolved independently on every operation. The underlying disk may be fixed or resolved once per operation when its configuration varies with the current context. These decorators fail closed on empty prefixes and reject unmapped calls so request- or tenant-scoped boundaries cannot be bypassed.
+
+Ported from: https://github.com/laravel/framework (illuminate/filesystem)
diff --git a/src/support/src/BinaryCodec.php b/src/support/src/BinaryCodec.php
index 54dadd08b7..77cb85cca5 100644
--- a/src/support/src/BinaryCodec.php
+++ b/src/support/src/BinaryCodec.php
@@ -33,6 +33,8 @@ public static function register(string $name, callable $encode, callable $decode
/**
* Encode a value to binary.
+ *
+ * @throws InvalidArgumentException
*/
public static function encode(Uuid|Ulid|string|null $value, string $format): ?string
{
@@ -63,6 +65,8 @@ public static function encode(Uuid|Ulid|string|null $value, string $format): ?st
/**
* Decode a binary value to string.
+ *
+ * @throws InvalidArgumentException
*/
public static function decode(?string $value, string $format): ?string
{
diff --git a/tests/Database/DatabaseEloquentAsBinaryCastTest.php b/tests/Database/DatabaseEloquentAsBinaryCastTest.php
index 369d145fa5..cfc3b774c4 100644
--- a/tests/Database/DatabaseEloquentAsBinaryCastTest.php
+++ b/tests/Database/DatabaseEloquentAsBinaryCastTest.php
@@ -122,6 +122,9 @@ class TestModel extends Model
{
protected array $guarded = [];
+ /**
+ * Get the attributes that should be cast.
+ */
protected function casts(): array
{
return [
diff --git a/tests/Integration/Filesystem/StorageFakeTest.php b/tests/Integration/Filesystem/StorageFakeTest.php
index c953445c3a..e54a4a2d44 100644
--- a/tests/Integration/Filesystem/StorageFakeTest.php
+++ b/tests/Integration/Filesystem/StorageFakeTest.php
@@ -8,10 +8,63 @@
use Hypervel\Support\Facades\ParallelTesting;
use Hypervel\Support\Facades\Storage;
use Hypervel\Testbench\TestCase;
+use League\Flysystem\UnableToReadFile;
class StorageFakeTest extends TestCase
{
- public function testFakePreservesOriginalDiskThrowConfig()
+ public function testFakeWhenDiskNotConfiguredDoesNotThrowExceptionOnError(): void
+ {
+ $result = Storage::fake('test')->get('nonExistentFile');
+
+ $this->assertNull($result);
+ }
+
+ public function testFakeWhenThrowSetToDiskThrowsExceptionOnError(): void
+ {
+ config(['filesystems.disks.test' => ['throw' => true]]);
+
+ $this->expectException(UnableToReadFile::class);
+ Storage::fake('test')->get('nonExistentFile');
+ }
+
+ public function testFakeWhenThrowOverwrittenUsesOverwrite(): void
+ {
+ config(['filesystems.disks.test' => ['throw' => true]]);
+
+ $result = Storage::fake('test', ['throw' => false])->get('nonExistentFile');
+ $this->assertNull($result);
+ }
+
+ public function testPersistentFakeWhenDiskNotConfiguredDoesNotThrowExceptionOnError(): void
+ {
+ $result = Storage::persistentFake('test')->get('nonExistentFile');
+
+ $this->assertNull($result);
+ }
+
+ public function testPersistentFakeWhenThrowSetToDiskThrowsExceptionOnError(): void
+ {
+ config(['filesystems.disks.test' => ['throw' => true]]);
+
+ $this->expectException(UnableToReadFile::class);
+ Storage::persistentFake('test')->get('nonExistentFile');
+ }
+
+ public function testPersistentFakeWhenThrowOverwrittenUsesOverwrite(): void
+ {
+ config(['filesystems.disks.test' => ['throw' => true]]);
+
+ $result = Storage::persistentFake('test', ['throw' => false])->get('nonExistentFile');
+ $this->assertNull($result);
+ }
+
+ public function testStorageFakeMethodsWithEnums(): void
+ {
+ $this->assertNull(Storage::persistentFake(StorageFakeStringDisk::Test)->get('nonExistentFile'));
+ $this->assertNull(Storage::fake(StorageFakeStringDisk::Public)->get('nonExistentFile'));
+ }
+
+ public function testFakePreservesOriginalDiskThrowConfig(): void
{
config(['filesystems.disks.local.throw' => true]);
@@ -21,7 +74,7 @@ public function testFakePreservesOriginalDiskThrowConfig()
$this->assertTrue($fake->getConfig()['throw']);
}
- public function testFakeDefaultsThrowToFalseWhenNotConfigured()
+ public function testFakeDefaultsThrowToFalseWhenNotConfigured(): void
{
config(['filesystems.disks.local' => ['driver' => 'local', 'root' => storage_path('app')]]);
@@ -31,7 +84,7 @@ public function testFakeDefaultsThrowToFalseWhenNotConfigured()
$this->assertFalse($fake->getConfig()['throw']);
}
- public function testFakeRegistersTemporaryUploadUrlBuilder()
+ public function testFakeRegistersTemporaryUploadUrlBuilder(): void
{
$fake = Storage::fake('local');
@@ -40,7 +93,7 @@ public function testFakeRegistersTemporaryUploadUrlBuilder()
$this->assertTrue($fake->providesTemporaryUploadUrls());
}
- public function testFakeTemporaryUploadUrlReturnsArrayWithUrlAndHeaders()
+ public function testFakeTemporaryUploadUrlReturnsArrayWithUrlAndHeaders(): void
{
$fake = Storage::fake('local');
@@ -52,7 +105,7 @@ public function testFakeTemporaryUploadUrlReturnsArrayWithUrlAndHeaders()
$this->assertArrayHasKey('headers', $result);
}
- public function testFakeUsesParallelTestingTokenSuffix()
+ public function testFakeUsesParallelTestingTokenSuffix(): void
{
ParallelTesting::resolveTokenUsing(fn () => '42');
@@ -68,7 +121,7 @@ public function testFakeUsesParallelTestingTokenSuffix()
}
}
- public function testPersistentFakePreservesOriginalDiskThrowConfig()
+ public function testPersistentFakePreservesOriginalDiskThrowConfig(): void
{
config(['filesystems.disks.local.throw' => true]);
@@ -111,3 +164,9 @@ enum StorageFakeDisk: int
{
case Zero = 0;
}
+
+enum StorageFakeStringDisk: string
+{
+ case Test = 'test';
+ case Public = 'public';
+}
diff --git a/tests/Support/SupportBinaryCodecTest.php b/tests/Support/SupportBinaryCodecTest.php
index bc9fe2525f..cb7902d643 100644
--- a/tests/Support/SupportBinaryCodecTest.php
+++ b/tests/Support/SupportBinaryCodecTest.php
@@ -63,19 +63,22 @@ public function testRegisterOverridesDefaultFormat(): void
}
#[DataProvider('nullAndBlankProvider')]
- public function testEncodeReturnsNullForNullAndBlank(mixed $value): void
+ public function testEncodeReturnsNullForNullAndBlank(?string $value): void
{
$this->assertNull(BinaryCodec::encode($value, 'uuid'));
$this->assertNull(BinaryCodec::encode($value, 'ulid'));
}
#[DataProvider('nullAndBlankProvider')]
- public function testDecodeReturnsNullForNullAndBlank(mixed $value): void
+ public function testDecodeReturnsNullForNullAndBlank(?string $value): void
{
$this->assertNull(BinaryCodec::decode($value, 'uuid'));
$this->assertNull(BinaryCodec::decode($value, 'ulid'));
}
+ /**
+ * Provide null and blank values.
+ */
public static function nullAndBlankProvider(): array
{
return [
@@ -209,6 +212,9 @@ public function testBlankBuiltInBinaryValuesRoundTrip(string $format, string $bi
$this->assertSame($binary, BinaryCodec::encode($text, $format));
}
+ /**
+ * Provide binary identifiers whose bytes are blank strings.
+ */
public static function blankBuiltInBinaryProvider(): array
{
return [
From 7dab54ea7f5cf40886da3ab690895b9de95e0a39 Mon Sep 17 00:00:00 2001
From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com>
Date: Wed, 9 Sep 2026 18:38:37 +0000
Subject: [PATCH 04/23] Accept closure subqueries across between query methods
Complete the supported subquery surface introduced by Laravel PRs #58290
and #58441. Both implementation bodies and their original regression tests
were already present, but Hypervel's eight native column parameter unions
rejected closures before the existing subquery parser could execute them.
Add Closure to all eight between/between-columns signatures and their
existing detailed annotations. Keep query construction, binding order and
all method bodies unchanged: createSub already executes closures against a
fresh query and retains only the resulting SQL and bindings.
Extend the two existing forwarding tests with closure inputs alongside
builder inputs, preserving their full SQL and binding assertions. Document
the public subquery argument with a correlated example; the pinned Laravel
docs do not yet describe it.
Upstream:
https://github.com/laravel/framework/pull/58290
https://github.com/laravel/framework/pull/58441
Source: 13.x at 01d008c9b5f32cb7c5e50a9a22273113d810b2a2
Docs: 2914ba0b06c6be40c2f1f992555853f6266707d6
Validation: full DatabaseQueryBuilderTest, scoped PHP-CS-Fixer, both
composer analyse passes, and git diff --check. Pre-fix probes reproduced
TypeError for all eight closure inputs; the regression assertions now
verify their SQL and binding order.
---
src/database/src/Query/Builder.php | 26 ++++-----
src/docs/queries.md | 18 +++++++
tests/Database/DatabaseQueryBuilderTest.php | 60 ++++++++++++---------
3 files changed, 65 insertions(+), 39 deletions(-)
diff --git a/src/database/src/Query/Builder.php b/src/database/src/Query/Builder.php
index d7dd751f13..dc9556f67e 100644
--- a/src/database/src/Query/Builder.php
+++ b/src/database/src/Query/Builder.php
@@ -1340,9 +1340,9 @@ public function whereNotNull(string|array|ExpressionContract $columns, string $b
/**
* Add a "where between" statement to the query.
*
- * @param \Hypervel\Database\Query\Builder|\Hypervel\Database\Eloquent\Builder<*>|\Hypervel\Database\Eloquent\Relations\Relation<*, *, *>|\Hypervel\Contracts\Database\Query\Expression|string $column
+ * @param \Closure|\Hypervel\Database\Query\Builder|\Hypervel\Database\Eloquent\Builder<*>|\Hypervel\Database\Eloquent\Relations\Relation<*, *, *>|\Hypervel\Contracts\Database\Query\Expression|string $column
*/
- public function whereBetween(self|EloquentBuilder|Relation|ExpressionContract|string $column, iterable $values, string $boolean = 'and', bool $not = false): static
+ public function whereBetween(Closure|self|EloquentBuilder|Relation|ExpressionContract|string $column, iterable $values, string $boolean = 'and', bool $not = false): static
{
$type = 'between';
@@ -1367,9 +1367,9 @@ public function whereBetween(self|EloquentBuilder|Relation|ExpressionContract|st
/**
* Add a "where between" statement using columns to the query.
*
- * @param \Hypervel\Database\Query\Builder|\Hypervel\Database\Eloquent\Builder<*>|\Hypervel\Database\Eloquent\Relations\Relation<*, *, *>|\Hypervel\Contracts\Database\Query\Expression|string $column
+ * @param \Closure|\Hypervel\Database\Query\Builder|\Hypervel\Database\Eloquent\Builder<*>|\Hypervel\Database\Eloquent\Relations\Relation<*, *, *>|\Hypervel\Contracts\Database\Query\Expression|string $column
*/
- public function whereBetweenColumns(self|EloquentBuilder|Relation|ExpressionContract|string $column, array $values, string $boolean = 'and', bool $not = false): static
+ public function whereBetweenColumns(Closure|self|EloquentBuilder|Relation|ExpressionContract|string $column, array $values, string $boolean = 'and', bool $not = false): static
{
$type = 'betweenColumns';
@@ -1388,9 +1388,9 @@ public function whereBetweenColumns(self|EloquentBuilder|Relation|ExpressionCont
/**
* Add an "or where between" statement to the query.
*
- * @param \Hypervel\Database\Query\Builder|\Hypervel\Database\Eloquent\Builder<*>|\Hypervel\Database\Eloquent\Relations\Relation<*, *, *>|\Hypervel\Contracts\Database\Query\Expression|string $column
+ * @param \Closure|\Hypervel\Database\Query\Builder|\Hypervel\Database\Eloquent\Builder<*>|\Hypervel\Database\Eloquent\Relations\Relation<*, *, *>|\Hypervel\Contracts\Database\Query\Expression|string $column
*/
- public function orWhereBetween(self|EloquentBuilder|Relation|ExpressionContract|string $column, iterable $values): static
+ public function orWhereBetween(Closure|self|EloquentBuilder|Relation|ExpressionContract|string $column, iterable $values): static
{
return $this->whereBetween($column, $values, 'or');
}
@@ -1398,7 +1398,7 @@ public function orWhereBetween(self|EloquentBuilder|Relation|ExpressionContract|
/**
* Add an "or where between" statement using columns to the query.
*/
- public function orWhereBetweenColumns(self|EloquentBuilder|Relation|ExpressionContract|string $column, array $values): static
+ public function orWhereBetweenColumns(Closure|self|EloquentBuilder|Relation|ExpressionContract|string $column, array $values): static
{
return $this->whereBetweenColumns($column, $values, 'or');
}
@@ -1406,9 +1406,9 @@ public function orWhereBetweenColumns(self|EloquentBuilder|Relation|ExpressionCo
/**
* Add a "where not between" statement to the query.
*
- * @param \Hypervel\Database\Query\Builder|\Hypervel\Database\Eloquent\Builder<*>|\Hypervel\Database\Eloquent\Relations\Relation<*, *, *>|\Hypervel\Contracts\Database\Query\Expression|string $column
+ * @param \Closure|\Hypervel\Database\Query\Builder|\Hypervel\Database\Eloquent\Builder<*>|\Hypervel\Database\Eloquent\Relations\Relation<*, *, *>|\Hypervel\Contracts\Database\Query\Expression|string $column
*/
- public function whereNotBetween(self|EloquentBuilder|Relation|ExpressionContract|string $column, iterable $values, string $boolean = 'and'): static
+ public function whereNotBetween(Closure|self|EloquentBuilder|Relation|ExpressionContract|string $column, iterable $values, string $boolean = 'and'): static
{
return $this->whereBetween($column, $values, $boolean, true);
}
@@ -1416,7 +1416,7 @@ public function whereNotBetween(self|EloquentBuilder|Relation|ExpressionContract
/**
* Add a "where not between" statement using columns to the query.
*/
- public function whereNotBetweenColumns(self|EloquentBuilder|Relation|ExpressionContract|string $column, array $values, string $boolean = 'and'): static
+ public function whereNotBetweenColumns(Closure|self|EloquentBuilder|Relation|ExpressionContract|string $column, array $values, string $boolean = 'and'): static
{
return $this->whereBetweenColumns($column, $values, $boolean, true);
}
@@ -1424,9 +1424,9 @@ public function whereNotBetweenColumns(self|EloquentBuilder|Relation|ExpressionC
/**
* Add an "or where not between" statement to the query.
*
- * @param \Hypervel\Database\Query\Builder|\Hypervel\Database\Eloquent\Builder<*>|\Hypervel\Database\Eloquent\Relations\Relation<*, *, *>|\Hypervel\Contracts\Database\Query\Expression|string $column
+ * @param \Closure|\Hypervel\Database\Query\Builder|\Hypervel\Database\Eloquent\Builder<*>|\Hypervel\Database\Eloquent\Relations\Relation<*, *, *>|\Hypervel\Contracts\Database\Query\Expression|string $column
*/
- public function orWhereNotBetween(self|EloquentBuilder|Relation|ExpressionContract|string $column, iterable $values): static
+ public function orWhereNotBetween(Closure|self|EloquentBuilder|Relation|ExpressionContract|string $column, iterable $values): static
{
return $this->whereNotBetween($column, $values, 'or');
}
@@ -1434,7 +1434,7 @@ public function orWhereNotBetween(self|EloquentBuilder|Relation|ExpressionContra
/**
* Add an "or where not between" statement using columns to the query.
*/
- public function orWhereNotBetweenColumns(self|EloquentBuilder|Relation|ExpressionContract|string $column, array $values): static
+ public function orWhereNotBetweenColumns(Closure|self|EloquentBuilder|Relation|ExpressionContract|string $column, array $values): static
{
return $this->whereNotBetweenColumns($column, $values, 'or');
}
diff --git a/src/docs/queries.md b/src/docs/queries.md
index 0498ba97ad..ac61025c9e 100644
--- a/src/docs/queries.md
+++ b/src/docs/queries.md
@@ -964,6 +964,22 @@ $users = DB::table('users')
->get();
```
+You may also pass a query builder or closure as the first argument to compare a subquery's result to the given values. For example, the following query retrieves users whose most recent score is between 50 and 100:
+
+```php
+use Hypervel\Database\Query\Builder;
+
+$users = DB::table('users')
+ ->whereBetween(function (Builder $query) {
+ $query->select('score')
+ ->from('scores')
+ ->whereColumn('scores.user_id', 'users.id')
+ ->orderByDesc('scores.created_at')
+ ->limit(1);
+ }, [50, 100])
+ ->get();
+```
+
**whereNotBetween / orWhereNotBetween**
The `whereNotBetween` method verifies that a column's value lies outside of two values:
@@ -992,6 +1008,8 @@ $patients = DB::table('patients')
->get();
```
+Like `whereBetween`, these methods also accept a query builder or closure as the first argument to compare a subquery's result to the two column values.
+
**whereValueBetween / whereValueNotBetween / orWhereValueBetween / orWhereValueNotBetween**
The `whereValueBetween` method verifies that a given value is between the values of two columns of the same type in the same table row:
diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php
index 5b35485264..bf2a53d54f 100755
--- a/tests/Database/DatabaseQueryBuilderTest.php
+++ b/tests/Database/DatabaseQueryBuilderTest.php
@@ -3231,38 +3231,46 @@ public function testWhereForwardersAcceptQueryBuilderSubqueries(): void
$this->assertSame([1, true, 5, true, 0, true, 3], $builder->getBindings());
}
- public function testBetweenForwardersAcceptQueryBuilderSubqueries(): void
+ public function testBetweenForwardersAcceptQueryableSubqueries(): void
{
- $subquery = $this->getBuilder()->select('score')->from('scores')->where('active', true);
- $builder = $this->getBuilder()
- ->from('parents')
- ->whereBetween($subquery, [1, 2])
- ->orWhereBetween($subquery, [3, 4])
- ->whereNotBetween($subquery, [5, 6])
- ->orWhereNotBetween($subquery, [7, 8]);
+ foreach ([
+ $this->getBuilder()->select('score')->from('scores')->where('active', true),
+ static fn (Builder $query): Builder => $query->select('score')->from('scores')->where('active', true),
+ ] as $subquery) {
+ $builder = $this->getBuilder()
+ ->from('parents')
+ ->whereBetween($subquery, [1, 2])
+ ->orWhereBetween($subquery, [3, 4])
+ ->whereNotBetween($subquery, [5, 6])
+ ->orWhereNotBetween($subquery, [7, 8]);
- $this->assertSame(
- 'select * from "parents" where (select "score" from "scores" where "active" = ?) between ? and ? or (select "score" from "scores" where "active" = ?) between ? and ? and (select "score" from "scores" where "active" = ?) not between ? and ? or (select "score" from "scores" where "active" = ?) not between ? and ?',
- $builder->toSql()
- );
- $this->assertSame([true, 1, 2, true, 3, 4, true, 5, 6, true, 7, 8], $builder->getBindings());
+ $this->assertSame(
+ 'select * from "parents" where (select "score" from "scores" where "active" = ?) between ? and ? or (select "score" from "scores" where "active" = ?) between ? and ? and (select "score" from "scores" where "active" = ?) not between ? and ? or (select "score" from "scores" where "active" = ?) not between ? and ?',
+ $builder->toSql()
+ );
+ $this->assertSame([true, 1, 2, true, 3, 4, true, 5, 6, true, 7, 8], $builder->getBindings());
+ }
}
- public function testBetweenColumnsForwardersAcceptQueryBuilderSubqueries(): void
+ public function testBetweenColumnsForwardersAcceptQueryableSubqueries(): void
{
- $subquery = $this->getBuilder()->select('score')->from('scores')->where('active', true);
- $builder = $this->getBuilder()
- ->from('parents')
- ->whereBetweenColumns($subquery, ['minimum', 'maximum'])
- ->orWhereBetweenColumns($subquery, ['minimum', 'maximum'])
- ->whereNotBetweenColumns($subquery, ['minimum', 'maximum'])
- ->orWhereNotBetweenColumns($subquery, ['minimum', 'maximum']);
+ foreach ([
+ $this->getBuilder()->select('score')->from('scores')->where('active', true),
+ static fn (Builder $query): Builder => $query->select('score')->from('scores')->where('active', true),
+ ] as $subquery) {
+ $builder = $this->getBuilder()
+ ->from('parents')
+ ->whereBetweenColumns($subquery, ['minimum', 'maximum'])
+ ->orWhereBetweenColumns($subquery, ['minimum', 'maximum'])
+ ->whereNotBetweenColumns($subquery, ['minimum', 'maximum'])
+ ->orWhereNotBetweenColumns($subquery, ['minimum', 'maximum']);
- $this->assertSame(
- 'select * from "parents" where (select "score" from "scores" where "active" = ?) between "minimum" and "maximum" or (select "score" from "scores" where "active" = ?) between "minimum" and "maximum" and (select "score" from "scores" where "active" = ?) not between "minimum" and "maximum" or (select "score" from "scores" where "active" = ?) not between "minimum" and "maximum"',
- $builder->toSql()
- );
- $this->assertSame([true, true, true, true], $builder->getBindings());
+ $this->assertSame(
+ 'select * from "parents" where (select "score" from "scores" where "active" = ?) between "minimum" and "maximum" or (select "score" from "scores" where "active" = ?) between "minimum" and "maximum" and (select "score" from "scores" where "active" = ?) not between "minimum" and "maximum" or (select "score" from "scores" where "active" = ?) not between "minimum" and "maximum"',
+ $builder->toSql()
+ );
+ $this->assertSame([true, true, true, true], $builder->getBindings());
+ }
}
public function testOrderForwardersAcceptQueryableSubqueries(): void
From 59140c25661b287a47b3b9a5998f8790aafb7d8b Mon Sep 17 00:00:00 2001
From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com>
Date: Wed, 9 Sep 2026 18:53:55 +0000
Subject: [PATCH 05/23] Complete appended validation rules and resource test
parity
Port the four missing current Laravel validation cases for escaped-dot
keys through constructor rules, appendRules and sometimes, and for
successive pipe-separated appended rules. The implementation already
handles these paths; preserve Hypervel's placeholder encoding, wildcard
expansion and cached lookup invalidation.
Restore addRules' upstream internal annotation and the resource collector's
LogicException annotation. Correct two misleading upstream comments, and
document appendRules at the public manually-created-validator surface.
Resource collection conversion and wrapping coverage is already present.
Correct the two inherited Request::create fixtures that reverse the URI
and HTTP method, and complete the touched method/provider docblocks.
Preserve every resource dataset and assertion. No production method body
or native signature changes.
Upstream:
https://github.com/laravel/framework/pull/58291
https://github.com/laravel/framework/pull/58304
https://github.com/laravel/framework/pull/58299
https://github.com/laravel/framework/pull/58302
Source: 13.x at 01d008c9b5f32cb7c5e50a9a22273113d810b2a2
Docs: 2914ba0b06c6be40c2f1f992555853f6266707d6
Validation: each changed test file passes (ValidationValidatorTest,
ResourceTest and ResourceCollectionTest); scoped configured PHP-CS-Fixer
and git diff --check pass. The final collection-stub docblock correction
was followed by another successful ResourceCollectionTest run.
---
src/docs/validation.md | 10 ++++++++++
src/http/src/Resources/CollectsResources.php | 2 ++
src/validation/src/Validator.php | 4 +++-
tests/Integration/Http/ResourceTest.php | 5 ++++-
.../Resources/Json/ResourceCollectionTest.php | 8 +++++++-
tests/Validation/ValidationValidatorTest.php | 20 +++++++++++++++++--
6 files changed, 44 insertions(+), 5 deletions(-)
diff --git a/src/docs/validation.md b/src/docs/validation.md
index 2545a4f328..4c8b31d2ba 100644
--- a/src/docs/validation.md
+++ b/src/docs/validation.md
@@ -998,6 +998,16 @@ The first argument passed to the `make` method is the data under validation. The
After determining whether the request validation failed, you may use the `withErrors` method to flash the error messages to the session. When using this method, the `$errors` variable will automatically be shared with your views after redirection, allowing you to easily display them back to the user. The `withErrors` method accepts a validator, a `MessageBag`, or a PHP `array`.
+#### Appending Rules
+
+Before running validation, you may use the `appendRules` method to add rules to an existing validator. The new rules are added to any rules already defined for each field:
+
+```php
+$validator->appendRules([
+ 'title' => 'min:5',
+]);
+```
+
#### Stopping on First Validation Failure
The `stopOnFirstFailure` method will inform the validator that it should stop validating all attributes once a single validation failure has occurred:
diff --git a/src/http/src/Resources/CollectsResources.php b/src/http/src/Resources/CollectsResources.php
index 217e7d09ed..7e452314c1 100644
--- a/src/http/src/Resources/CollectsResources.php
+++ b/src/http/src/Resources/CollectsResources.php
@@ -52,6 +52,8 @@ protected function collectResource(mixed $resource): mixed
* Get the resource that this resource collects.
*
* @return null|class-string<\Hypervel\Http\Resources\Json\JsonResource>
+ *
+ * @throws LogicException
*/
protected function collects(): ?string
{
diff --git a/src/validation/src/Validator.php b/src/validation/src/Validator.php
index 25b49633b6..8727d6619a 100644
--- a/src/validation/src/Validator.php
+++ b/src/validation/src/Validator.php
@@ -1671,10 +1671,12 @@ public function appendRules(array $rules): static
/**
* Parse the given rules and merge them into current rules.
+ *
+ * @internal
*/
public function addRules(array $rules): void
{
- // The primary purpose of this parser is to expand any "*" rules to the all
+ // The primary purpose of this parser is to expand any "*" rules to all
// of the explicit rules needed for the given data. For example the rule
// names.* would get expanded to names.0, names.1, etc. for this data.
$response = (new ValidationRuleParser($this->data))
diff --git a/tests/Integration/Http/ResourceTest.php b/tests/Integration/Http/ResourceTest.php
index c67f949342..0ac0d5bc19 100644
--- a/tests/Integration/Http/ResourceTest.php
+++ b/tests/Integration/Http/ResourceTest.php
@@ -59,6 +59,9 @@ class ResourceTest extends TestCase
public function testResourceMayBeConvertedToArray(): void
{
$resource = new class((new User)->forceFill(['id' => 1, 'name' => 'Taylor Otwell'])) extends JsonResource {
+ /**
+ * Transform the resource into an array.
+ */
public function toArray(Request $request): array
{
return [
@@ -80,7 +83,7 @@ public function toArray(Request $request): array
}
};
- $request = Request::create('GET', '/users');
+ $request = Request::create('/users', 'GET');
tap($resource->toArray($request), function ($userAsArray) use ($request) {
$this->assertSame(1, $userAsArray['id']);
diff --git a/tests/Integration/Http/Resources/Json/ResourceCollectionTest.php b/tests/Integration/Http/Resources/Json/ResourceCollectionTest.php
index c8355885b5..66b2c44fd4 100644
--- a/tests/Integration/Http/Resources/Json/ResourceCollectionTest.php
+++ b/tests/Integration/Http/Resources/Json/ResourceCollectionTest.php
@@ -16,11 +16,14 @@ class ResourceCollectionTest extends TestCase
#[DataProvider('toArrayDataProvider')]
public function testItCanReturnToArray(ResourceCollection $collection, mixed $expected): void
{
- $request = Request::create('GET', '/');
+ $request = Request::create('/', 'GET');
$this->assertSame($expected, $collection->toArray($request));
}
+ /**
+ * Provide resource collections and their expected arrays.
+ */
public static function toArrayDataProvider(): iterable
{
yield [
@@ -49,6 +52,9 @@ public static function toArrayDataProvider(): iterable
yield [
new class(['list' => new Fluent(['id' => 1]), 'total' => 1]) extends ResourceCollection {
+ /**
+ * Transform the resource into a JSON array.
+ */
public function toArray(Request $request): array
{
return $this->resource->toArray();
diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php
index e710cb8c95..c87cb5859a 100755
--- a/tests/Validation/ValidationValidatorTest.php
+++ b/tests/Validation/ValidationValidatorTest.php
@@ -7782,7 +7782,7 @@ public function testValidateImplicitEachWithAsterisksForRequiredNonExistingKey()
$this->assertFalse($v->passes());
}
- public function testParsingArrayKeysWithDot()
+ public function testParsingArrayKeysWithDot(): void
{
$trans = $this->getArrayTranslator();
// Interpreted dot fails on empty value
@@ -7791,7 +7791,7 @@ public function testParsingArrayKeysWithDot()
// Escaped dot fails on empty value
$v = new Validator($trans, ['foo' => ['bar' => 'valid'], 'foo.bar' => ''], ['foo\.bar' => 'required']);
$this->assertTrue($v->fails());
- // Interpreted dot succeeds
+ // Escaped dot succeeds
$v = new Validator($trans, ['foo' => ['bar' => 'valid'], 'foo.bar' => 'zxc'], ['foo\.bar' => 'required']);
$this->assertFalse($v->fails());
// Interpreted dot followed by escaped dot fails on empty value
@@ -7800,6 +7800,22 @@ public function testParsingArrayKeysWithDot()
// Interpreted dot followed by escaped dot fails on empty value
$v = new Validator($trans, ['foo' => [['bar.baz' => ''], ['bar.baz' => '']]], ['foo.*.bar\.baz' => 'required']);
$this->assertTrue($v->fails());
+
+ $v = new Validator($trans, ['foo.bar' => 'valid'], ['foo\.bar' => 'required']);
+ $this->assertFalse($v->fails());
+
+ $v = new Validator($trans, ['foo.bar' => 'valid'], []);
+ $v->appendRules(['foo\.bar' => 'required']);
+ $this->assertFalse($v->fails());
+
+ $v = new Validator($trans, ['foo.bar' => 'valid'], []);
+ $v->sometimes('foo\.bar', 'required', fn (): bool => true);
+ $this->assertFalse($v->fails());
+
+ $v = new Validator($trans, ['name' => 'ab'], ['name' => 'required']);
+ $v->appendRules(['name' => 'string']);
+ $v->appendRules(['name' => 'min:5|max:255']);
+ $this->assertTrue($v->fails());
}
public function testParsingArrayKeysWithAsterisk(): void
From c5adaab22339e344b8e07fb924bb480521481aff Mon Sep 17 00:00:00 2001
From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com>
Date: Wed, 9 Sep 2026 19:39:33 +0000
Subject: [PATCH 06/23] Complete array filtering and cloned connection test
parity
Bring the remaining tests and documentation for Laravel #58317, #58288 and #58311 into parity with framework source 01d008c9b5f32cb7c5e50a9a22273113d810b2a2 and docs source 2914ba0b06c6be40c2f1f992555853f6266707d6.
Port the five strict empty-array assertions and correct four array-filtering examples to show preserved keys. Replace three mislabeled SELECT assertions with actual insert, update and delete calls through the cloned connection in pretend mode. Assert the interpolated SQL emitted by Hypervel and retain schema and original-connection isolation coverage. Type the local test helpers and remove stale Laravel annotations.
Clarify that isolated Blade includes still receive shared view data. The array examples, duplicated SELECT checks and overly restrictive Blade wording also exist upstream; runtime behavior remains unchanged.
Validated both affected PHPUnit classes immediately, their combined focused ParaTest run, scoped PHP-CS-Fixer and diff checks.
Upstream: https://github.com/laravel/framework/pull/58317
https://github.com/laravel/framework/pull/58288
https://github.com/laravel/framework/pull/58311
https://github.com/laravel/docs/pull/10999
---
src/docs/blade.md | 2 +-
src/docs/helpers.md | 12 ++---
...EloquentIntegrationWithTablePrefixTest.php | 44 +++++++++----------
tests/Support/SupportArrTest.php | 10 ++---
4 files changed, 33 insertions(+), 35 deletions(-)
diff --git a/src/docs/blade.md b/src/docs/blade.md
index 6bbc3b6e1c..4429f63008 100644
--- a/src/docs/blade.md
+++ b/src/docs/blade.md
@@ -586,7 +586,7 @@ To include the first view that exists from a given array of views, you may use t
@includeFirst(['custom.admin', 'admin'], ['status' => 'complete'])
```
-If you would like to include a view without inheriting any variables from the parent view, you may use the `@includeIsolated` directive. The included view will only have access to variables you explicitly pass:
+If you would like to include a view without inheriting any variables from the parent view, you may use the `@includeIsolated` directive. Variables shared with all views remain available, and you may pass additional data as the second argument:
```blade
@includeIsolated('view.name', ['user' => $user])
diff --git a/src/docs/helpers.md b/src/docs/helpers.md
index 5b3e0fc2c6..753ccff0f2 100644
--- a/src/docs/helpers.md
+++ b/src/docs/helpers.md
@@ -452,7 +452,7 @@ $filtered = Arr::except($array, ['price']);
#### `Arr::exceptValues()` {.collection-method}
-The `Arr::exceptValues` method removes the specified values from an array:
+The `Arr::exceptValues` method removes the specified values from an array, preserving the original keys:
```php
use Hypervel\Support\Arr;
@@ -461,7 +461,7 @@ $array = ['foo', 'bar', 'baz', 'qux'];
$filtered = Arr::exceptValues($array, ['foo', 'baz']);
-// ['bar', 'qux']
+// [1 => 'bar', 3 => 'qux']
```
You may also pass `true` to the `strict` argument to use strict type comparisons when filtering:
@@ -473,7 +473,7 @@ $array = [1, '1', 2, '2'];
$filtered = Arr::exceptValues($array, [1, 2], strict: true);
-// ['1', '2']
+// [1 => '1', 3 => '2']
```
@@ -889,7 +889,7 @@ $slice = Arr::only($array, ['name', 'price']);
#### `Arr::onlyValues()` {.collection-method}
-The `Arr::onlyValues` method returns only the specified values from an array:
+The `Arr::onlyValues` method returns only the specified values from an array, preserving the original keys:
```php
use Hypervel\Support\Arr;
@@ -898,7 +898,7 @@ $array = ['foo', 'bar', 'baz', 'qux'];
$filtered = Arr::onlyValues($array, ['foo', 'baz']);
-// ['foo', 'baz']
+// [0 => 'foo', 2 => 'baz']
```
You may also pass `true` to the `strict` argument to use strict type comparisons when filtering:
@@ -910,7 +910,7 @@ $array = [1, '1', 2, '2'];
$filtered = Arr::onlyValues($array, [1, 2], strict: true);
-// [1, 2]
+// [0 => 1, 2 => 2]
```
diff --git a/tests/Database/DatabaseEloquentIntegrationWithTablePrefixTest.php b/tests/Database/DatabaseEloquentIntegrationWithTablePrefixTest.php
index 69e65f7bc8..ac063cb5ae 100644
--- a/tests/Database/DatabaseEloquentIntegrationWithTablePrefixTest.php
+++ b/tests/Database/DatabaseEloquentIntegrationWithTablePrefixTest.php
@@ -5,9 +5,11 @@
namespace Hypervel\Tests\Database\DatabaseEloquentIntegrationWithTablePrefixTest;
use Hypervel\Database\Capsule\Manager as DB;
+use Hypervel\Database\Connection;
use Hypervel\Database\Eloquent\Collection;
use Hypervel\Database\Eloquent\Model as Eloquent;
use Hypervel\Database\Eloquent\Relations\Relation;
+use Hypervel\Database\Schema\Builder;
use Hypervel\Testbench\TestCase;
class DatabaseEloquentIntegrationWithTablePrefixTest extends TestCase
@@ -34,7 +36,10 @@ protected function setUp(): void
$this->createSchema();
}
- protected function createSchema()
+ /**
+ * Create the database schema.
+ */
+ protected function createSchema(): void
{
$this->schema('default')->create('users', function ($table) {
$table->increments('id');
@@ -80,7 +85,7 @@ protected function tearDown(): void
parent::tearDown();
}
- public function testBasicModelHydration()
+ public function testBasicModelHydration(): void
{
User::create(['email' => 'taylorotwell@gmail.com']);
User::create(['email' => 'abigailotwell@gmail.com']);
@@ -93,7 +98,7 @@ public function testBasicModelHydration()
$this->assertCount(1, $models);
}
- public function testTablePrefixWithClonedConnection()
+ public function testTablePrefixWithClonedConnection(): void
{
$originalConnection = $this->connection();
$originalPrefix = $originalConnection->getTablePrefix();
@@ -116,7 +121,7 @@ public function testTablePrefixWithClonedConnection()
$clonedConnection->getSchemaBuilder()->drop('test_table');
}
- public function testQueryGrammarUsesCorrectPrefixAfterCloning()
+ public function testQueryGrammarUsesCorrectPrefixAfterCloning(): void
{
$originalConnection = $this->connection();
@@ -126,42 +131,35 @@ public function testQueryGrammarUsesCorrectPrefixAfterCloning()
$selectSql = $clonedConnection->table('users')->toSql();
$this->assertStringContainsString('new_prefix_users', $selectSql);
- $insertSql = $clonedConnection->table('users')->toSql();
- $this->assertStringContainsString('new_prefix_users', $insertSql);
-
- $updateSql = $clonedConnection->table('users')->where('id', 1)->toSql();
- $this->assertStringContainsString('new_prefix_users', $updateSql);
+ $queries = $clonedConnection->pretend(function (Connection $connection): void {
+ $connection->table('users')->insert(['email' => 'taylor@example.com']);
+ $connection->table('users')->where('id', 1)->update(['email' => 'abigail@example.com']);
+ $connection->table('users')->where('id', 1)->delete();
+ });
- $deleteSql = $clonedConnection->table('users')->where('id', 1)->toSql();
- $this->assertStringContainsString('new_prefix_users', $deleteSql);
+ $this->assertSame([
+ 'insert into "new_prefix_users" ("email") values (\'taylor@example.com\')',
+ 'update "new_prefix_users" set "email" = \'abigail@example.com\' where "id" = 1',
+ 'delete from "new_prefix_users" where "id" = 1',
+ ], array_column($queries, 'query'));
$originalSql = $originalConnection->table('users')->toSql();
$this->assertStringContainsString('prefix_users', $originalSql);
$this->assertStringNotContainsString('new_prefix_users', $originalSql);
}
- /**
- * Helpers...
- * @param mixed $connection
- */
-
/**
* Get a database connection instance.
- *
- * @return \Illuminate\Database\Connection
*/
- protected function connection($connection = 'default')
+ protected function connection(string $connection = 'default'): Connection
{
return Eloquent::getConnectionResolver()->connection($connection);
}
/**
* Get a schema builder instance.
- *
- * @param mixed $connection
- * @return \Illuminate\Database\Schema\Builder
*/
- protected function schema($connection = 'default')
+ protected function schema(string $connection = 'default'): Builder
{
return $this->connection($connection)->getSchemaBuilder();
}
diff --git a/tests/Support/SupportArrTest.php b/tests/Support/SupportArrTest.php
index bb9bfcb5bb..aae8298bb0 100644
--- a/tests/Support/SupportArrTest.php
+++ b/tests/Support/SupportArrTest.php
@@ -365,16 +365,16 @@ public function testExceptValues(): void
$array = ['a' => 1, 'b' => 2, 'c' => 1, 'd' => 3];
$this->assertEquals(['b' => 2, 'd' => 3], Arr::exceptValues($array, 1));
- $this->assertEquals([], Arr::exceptValues([], 'foo'));
+ $this->assertSame([], Arr::exceptValues([], 'foo'));
$this->assertEquals(['foo', 'bar'], Arr::exceptValues(['foo', 'bar'], []));
$array = [1, '1', 2, '2', 3];
$this->assertEquals([1 => '1', 3 => '2'], Arr::exceptValues($array, [1, 2, 3], true));
- $this->assertEquals([], Arr::exceptValues($array, [1, 2, 3]));
+ $this->assertSame([], Arr::exceptValues($array, [1, 2, 3]));
$array = ['a' => true, 'b' => false, 'c' => 1, 'd' => 0];
$this->assertEquals(['a' => true, 'b' => false], Arr::exceptValues($array, [1, 0], true));
- $this->assertEquals([], Arr::exceptValues($array, [1, 0]));
+ $this->assertSame([], Arr::exceptValues($array, [1, 0]));
}
public function testExists(): void
@@ -1076,8 +1076,8 @@ public function testOnlyValues(): void
$array = ['a' => 1, 'b' => 2, 'c' => 1, 'd' => 3];
$this->assertEquals(['a' => 1, 'c' => 1], Arr::onlyValues($array, 1));
- $this->assertEquals([], Arr::onlyValues([], 'foo'));
- $this->assertEquals([], Arr::onlyValues(['foo', 'bar'], []));
+ $this->assertSame([], Arr::onlyValues([], 'foo'));
+ $this->assertSame([], Arr::onlyValues(['foo', 'bar'], []));
$array = [1, '1', 2, '2', 3];
$this->assertEquals([0 => 1, 2 => 2, 4 => 3], Arr::onlyValues($array, [1, 2, 3], true));
From d6c3f41a892c5ca78c6313c9ba4cb0a43ce08d49 Mon Sep 17 00:00:00 2001
From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com>
Date: Wed, 9 Sep 2026 20:09:06 +0000
Subject: [PATCH 07/23] Complete migration and schema typing from Laravel
Port the current migration repository record shapes, batch maps, creation
and connection resolver callbacks, Blueprint callbacks, schema return
inference, and MySQL DDL lock value annotations. Include the later fixes
for malformed unions and incorrect array/object annotations rather than
reproducing the original typing defects.
Creation hooks accept a nullable table name for generic migrations. Keep
the prospective migration class name typed as string: the caller checks
whether that class exists. Preserve coroutine connection routing and
physical-session foreign-key suppression without changing runtime APIs.
Document the creation hook's worker lifetime and share schema defaults
with the existing static reset through typed constants.
Correct the inherited MySQL DDL example: ALGORITHM=INSTANT permits only
LOCK=DEFAULT. Keep the upstream compiler tests that verify clause
forwarding. Remove unsupported SQL Server-only definition metadata.
Add type fixtures for public repository records, callback inference,
schema refinements, and exact first-class callable lock signatures.
Schema facade generation remains unchanged because the generator
simplifies these refinements; no manual facade changes are included.
Upstream source: laravel/framework 13.x at
01d008c9b5f32cb7c5e50a9a22273113d810b2a2.
https://github.com/laravel/framework/pull/58293
https://github.com/laravel/framework/pull/58561
https://github.com/laravel/framework/pull/58624
https://github.com/laravel/framework/pull/58638
https://github.com/laravel/framework/pull/59875
https://github.com/laravel/framework/pull/59876
https://github.com/laravel/framework/pull/59887
Verified with full source/type analysis, focused migration and schema
tests, formatting, and facade-generation lint.
---
.../DatabaseMigrationRepository.php | 12 +++++
.../src/Migrations/MigrationCreator.php | 7 +++
.../MigrationRepositoryInterface.php | 12 +++++
src/database/src/Migrations/Migrator.php | 5 ++
src/database/src/Schema/Blueprint.php | 4 ++
src/database/src/Schema/Builder.php | 49 +++++++++++++------
src/database/src/Schema/ColumnDefinition.php | 3 +-
.../src/Schema/ForeignKeyDefinition.php | 4 +-
src/database/src/Schema/IndexDefinition.php | 4 +-
src/docs/migrations.md | 6 +--
types/Database/Migrations.php | 46 +++++++++++++++++
types/Database/Schema.php | 38 ++++++++++++++
12 files changed, 165 insertions(+), 25 deletions(-)
create mode 100644 types/Database/Migrations.php
diff --git a/src/database/src/Migrations/DatabaseMigrationRepository.php b/src/database/src/Migrations/DatabaseMigrationRepository.php
index 156e9ec952..8a33fd7ba6 100755
--- a/src/database/src/Migrations/DatabaseMigrationRepository.php
+++ b/src/database/src/Migrations/DatabaseMigrationRepository.php
@@ -26,6 +26,8 @@ public function __construct(
/**
* Get the completed migrations.
+ *
+ * @return string[]
*/
public function getRan(): array
{
@@ -37,6 +39,8 @@ public function getRan(): array
/**
* Get the list of migrations.
+ *
+ * @return object{id: int, migration: string, batch: int}[]
*/
public function getMigrations(int $steps): array
{
@@ -51,6 +55,8 @@ public function getMigrations(int $steps): array
/**
* Get the list of the migrations by batch number.
+ *
+ * @return object{id: int, migration: string, batch: int}[]
*/
public function getMigrationsByBatch(int $batch): array
{
@@ -63,6 +69,8 @@ public function getMigrationsByBatch(int $batch): array
/**
* Get the last migration batch.
+ *
+ * @return object{id: int, migration: string, batch: int}[]
*/
public function getLast(): array
{
@@ -73,6 +81,8 @@ public function getLast(): array
/**
* Get the completed migrations with their batch numbers.
+ *
+ * @return array
*/
public function getMigrationBatches(): array
{
@@ -94,6 +104,8 @@ public function log(string $file, int $batch): void
/**
* Remove a migration from the log.
+ *
+ * @param object{id?: int, migration: string, batch?: int} $migration
*/
public function delete(object $migration): void
{
diff --git a/src/database/src/Migrations/MigrationCreator.php b/src/database/src/Migrations/MigrationCreator.php
index 622c18d2d4..9574394dce 100644
--- a/src/database/src/Migrations/MigrationCreator.php
+++ b/src/database/src/Migrations/MigrationCreator.php
@@ -19,6 +19,8 @@ class MigrationCreator
/**
* The registered post create hooks.
+ *
+ * @var (Closure(?string, string): void)[]
*/
protected array $postCreate = [];
@@ -169,6 +171,11 @@ protected function firePostCreateHooks(?string $table, string $path): void
/**
* Register a post migration create hook.
+ *
+ * Boot-only. Hooks persist on the migration creator for the worker
+ * lifetime and run for every subsequent migration creation.
+ *
+ * @param Closure(?string, string): void $callback
*/
public function afterCreate(Closure $callback): void
{
diff --git a/src/database/src/Migrations/MigrationRepositoryInterface.php b/src/database/src/Migrations/MigrationRepositoryInterface.php
index 147941a14e..9de783476b 100755
--- a/src/database/src/Migrations/MigrationRepositoryInterface.php
+++ b/src/database/src/Migrations/MigrationRepositoryInterface.php
@@ -8,26 +8,36 @@ interface MigrationRepositoryInterface
{
/**
* Get the completed migrations.
+ *
+ * @return string[]
*/
public function getRan(): array;
/**
* Get the list of migrations.
+ *
+ * @return object{id: int, migration: string, batch: int}[]
*/
public function getMigrations(int $steps): array;
/**
* Get the list of the migrations by batch.
+ *
+ * @return object{id: int, migration: string, batch: int}[]
*/
public function getMigrationsByBatch(int $batch): array;
/**
* Get the last migration batch.
+ *
+ * @return object{id: int, migration: string, batch: int}[]
*/
public function getLast(): array;
/**
* Get the completed migrations with their batch numbers.
+ *
+ * @return array
*/
public function getMigrationBatches(): array;
@@ -38,6 +48,8 @@ public function log(string $file, int $batch): void;
/**
* Remove a migration from the log.
+ *
+ * @param object{id?: int, migration: string, batch?: int} $migration
*/
public function delete(object $migration): void;
diff --git a/src/database/src/Migrations/Migrator.php b/src/database/src/Migrations/Migrator.php
index e402964267..99f28139f4 100755
--- a/src/database/src/Migrations/Migrator.php
+++ b/src/database/src/Migrations/Migrator.php
@@ -35,6 +35,8 @@ class Migrator
{
/**
* The custom connection resolver callback.
+ *
+ * @var null|(Closure(Resolver, ?string): Connection)
*/
protected static ?Closure $connectionResolverCallback = null;
@@ -263,6 +265,7 @@ public function rollback(array|string $paths = [], array $options = []): array
* Get the migrations for a rollback operation.
*
* @param array $options
+ * @return object{id: int, migration: string, batch: int}[]
*/
protected function getMigrationsForRollback(array $options): array
{
@@ -799,6 +802,8 @@ public function resolveConnection(?string $connection): Connection
*
* Boot-only. The callback persists in a static property for the worker
* lifetime and runs on every migration's connection resolution.
+ *
+ * @param Closure(Resolver, ?string): Connection $callback
*/
public static function resolveConnectionsUsing(Closure $callback): void
{
diff --git a/src/database/src/Schema/Blueprint.php b/src/database/src/Schema/Blueprint.php
index fdc6ddf931..d2e4309fcc 100755
--- a/src/database/src/Schema/Blueprint.php
+++ b/src/database/src/Schema/Blueprint.php
@@ -88,6 +88,8 @@ class Blueprint
/**
* Create a new schema blueprint.
+ *
+ * @param null|(Closure(self): void) $callback
*/
public function __construct(Connection $connection, string $table, ?Closure $callback = null)
{
@@ -1524,6 +1526,8 @@ protected function addColumnDefinition(ColumnDefinition $definition): ColumnDefi
/**
* Add the columns from the callback after the given column.
+ *
+ * @param Closure(self): void $callback
*/
public function after(string $column, Closure $callback): void
{
diff --git a/src/database/src/Schema/Builder.php b/src/database/src/Schema/Builder.php
index ad297f980b..9febedbcb4 100755
--- a/src/database/src/Schema/Builder.php
+++ b/src/database/src/Schema/Builder.php
@@ -17,6 +17,12 @@ class Builder
{
use Macroable;
+ protected const int DEFAULT_STRING_LENGTH = 255;
+
+ protected const int DEFAULT_TIME_PRECISION = 0;
+
+ protected const string DEFAULT_MORPH_KEY_TYPE = 'int';
+
/**
* The database connection instance.
*/
@@ -36,18 +42,22 @@ class Builder
/**
* The default string length for migrations.
+ *
+ * @var null|non-negative-int
*/
- public static ?int $defaultStringLength = 255;
+ public static ?int $defaultStringLength = self::DEFAULT_STRING_LENGTH;
/**
* The default time precision for migrations.
*/
- public static ?int $defaultTimePrecision = 0;
+ public static ?int $defaultTimePrecision = self::DEFAULT_TIME_PRECISION;
/**
* The default relationship morph key type.
+ *
+ * @var 'int'|'ulid'|'uuid'
*/
- public static string $defaultMorphKeyType = 'int';
+ public static string $defaultMorphKeyType = self::DEFAULT_MORPH_KEY_TYPE;
/**
* Create a new database Schema manager.
@@ -63,6 +73,8 @@ public function __construct(Connection $connection)
*
* Boot-only. The length persists in a static property for the worker
* lifetime and applies to every Blueprint::string() across all coroutines.
+ *
+ * @param non-negative-int $length
*/
public static function defaultStringLength(int $length): void
{
@@ -97,17 +109,6 @@ public static function defaultMorphKeyType(string $type): void
static::$defaultMorphKeyType = $type;
}
- /**
- * Flush all static state.
- */
- public static function flushState(): void
- {
- static::$defaultStringLength = 255;
- static::$defaultTimePrecision = 0;
- static::$defaultMorphKeyType = 'int';
- static::flushMacros();
- }
-
/**
* Set the default morph key type for migrations to UUIDs.
*
@@ -575,6 +576,11 @@ public function disableForeignKeyConstraints(): bool
/**
* Disable foreign key constraints during the execution of a callback.
+ *
+ * @template TReturn
+ *
+ * @param Closure(): TReturn $callback
+ * @return TReturn
*/
public function withoutForeignKeyConstraints(Closure $callback): mixed
{
@@ -750,6 +756,10 @@ public function getCurrentSchemaName(): ?string
/**
* Parse the given database object reference and extract the schema and table.
+ *
+ * @return array{null|string, string}
+ *
+ * @throws InvalidArgumentException
*/
public function parseSchemaAndTable(string $reference, bool|string|null $withDefaultSchema = null): array
{
@@ -790,4 +800,15 @@ public function blueprintResolver(Closure $resolver): void
{
$this->resolver = $resolver;
}
+
+ /**
+ * Flush all static state.
+ */
+ public static function flushState(): void
+ {
+ static::$defaultStringLength = self::DEFAULT_STRING_LENGTH;
+ static::$defaultTimePrecision = self::DEFAULT_TIME_PRECISION;
+ static::$defaultMorphKeyType = self::DEFAULT_MORPH_KEY_TYPE;
+ static::flushMacros();
+ }
}
diff --git a/src/database/src/Schema/ColumnDefinition.php b/src/database/src/Schema/ColumnDefinition.php
index 40a9381dd9..a62b767957 100644
--- a/src/database/src/Schema/ColumnDefinition.php
+++ b/src/database/src/Schema/ColumnDefinition.php
@@ -23,9 +23,8 @@
* @method $this instant() Specify that algorithm=instant should be used for the column operation (MySQL)
* @method $this index(bool|string $indexName = null) Add an index
* @method $this invisible() Specify that the column should be invisible to "SELECT *" (MySQL)
- * @method $this lock(string $value) Specify the DDL lock mode for the column operation (MySQL)
+ * @method $this lock(('default'|'exclusive'|'none'|'shared') $value) Specify the DDL lock mode for the column operation (MySQL)
* @method $this nullable(bool $value = true) Allow NULL values to be inserted into the column
- * @method $this persisted() Mark the computed generated column as persistent (SQL Server)
* @method $this primary(bool $value = true) Add a primary index
* @method $this spatialIndex(bool|string $indexName = null) Add a spatial index
* @method $this vectorIndex(bool|string $indexName = null) Add a vector index
diff --git a/src/database/src/Schema/ForeignKeyDefinition.php b/src/database/src/Schema/ForeignKeyDefinition.php
index 8299fe0609..47a255bb83 100644
--- a/src/database/src/Schema/ForeignKeyDefinition.php
+++ b/src/database/src/Schema/ForeignKeyDefinition.php
@@ -9,11 +9,11 @@
/**
* @method ForeignKeyDefinition deferrable(bool $value = true) Set the foreign key as deferrable (PostgreSQL)
* @method ForeignKeyDefinition initiallyImmediate(bool $value = true) Set the default time to check the constraint (PostgreSQL)
- * @method ForeignKeyDefinition lock(string $value) Specify the DDL lock mode for the foreign key operation (MySQL)
+ * @method ForeignKeyDefinition lock(('default'|'exclusive'|'none'|'shared') $value) Specify the DDL lock mode for the foreign key operation (MySQL)
* @method ForeignKeyDefinition on(string $table) Specify the referenced table
* @method ForeignKeyDefinition onDelete(string $action) Add an ON DELETE action
* @method ForeignKeyDefinition onUpdate(string $action) Add an ON UPDATE action
- * @method ForeignKeyDefinition references(array|string $columns) Specify the referenced column(s)
+ * @method ForeignKeyDefinition references(string|string[] $columns) Specify the referenced column(s)
*/
class ForeignKeyDefinition extends Fluent
{
diff --git a/src/database/src/Schema/IndexDefinition.php b/src/database/src/Schema/IndexDefinition.php
index 3f6b883fe4..b7b9bf7f56 100644
--- a/src/database/src/Schema/IndexDefinition.php
+++ b/src/database/src/Schema/IndexDefinition.php
@@ -12,9 +12,9 @@
* @method $this deferrable(bool $value = true) Specify that the unique index is deferrable (PostgreSQL)
* @method $this initiallyImmediate(bool $value = true) Specify the default time to check the unique index constraint (PostgreSQL)
* @method $this language(string $language) Specify a language for the full text index (PostgreSQL)
- * @method $this lock(string $value) Specify the DDL lock mode for the index operation (MySQL)
+ * @method $this lock(('default'|'exclusive'|'none'|'shared') $value) Specify the DDL lock mode for the index operation (MySQL)
* @method $this nullsNotDistinct(bool $value = true) Specify that the null values should not be treated as distinct (PostgreSQL)
- * @method $this online(bool $value = true) Specify that index creation should not lock the table (PostgreSQL/SqlServer)
+ * @method $this online(bool $value = true) Specify that index creation should not lock the table (PostgreSQL)
*/
class IndexDefinition extends Fluent
{
diff --git a/src/docs/migrations.md b/src/docs/migrations.md
index 6d3ff8a855..1a5fa7f6ff 100644
--- a/src/docs/migrations.md
+++ b/src/docs/migrations.md
@@ -1405,11 +1405,7 @@ $table->string('name')->lock('none');
$table->index('email')->lock('shared');
```
-If the requested lock mode is incompatible with the operation, MySQL will raise an error. The `lock` modifier may be combined with the `instant` modifier to further optimize schema changes:
-
-```php
-$table->string('name')->instant()->lock('none');
-```
+If the requested lock mode is incompatible with the operation, MySQL will raise an error. When the `instant` modifier is used, MySQL permits only the `default` lock mode.
### Modifying Columns
diff --git a/types/Database/Migrations.php b/types/Database/Migrations.php
new file mode 100644
index 0000000000..fb3deef27e
--- /dev/null
+++ b/types/Database/Migrations.php
@@ -0,0 +1,46 @@
+', $repository->getRan());
+ assertType('array', $database->getRan());
+ assertType('array', $repository->getMigrationBatches());
+ assertType('array', $database->getMigrationBatches());
+
+ assertType('array