Skip to content
1 change: 1 addition & 0 deletions src/database/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Documentation: https://hypervel.org/docs/database
- Laravel's `migrate:fresh` command wipes only its selected connection. Hypervel discovers the connection declared by each migration and wipes every resolved target before rebuilding the schema.
- Laravel's deprecated database-inspection forwarding helpers are intentionally not ported. Extensions can call `ConnectionInterface::getDriverTitle()` and `threadCount()` directly.
- Laravel's remaining directly deprecated Database compatibility forwarders are intentionally not ported. Use the current class-keyed factory resolver, schema blueprint and grammar APIs, and correctly named PostgreSQL truncation method instead.
- Laravel's `ConnectionResolver` is a simple registry of connection instances. Hypervel's `ConnectionResolver` is the pooled resolver built by the container, so it has no array constructor or `addConnection()` method. Tests that inject existing connections can mock `ConnectionResolverInterface`, while tests that need real configured connections can use the Capsule manager.
- Laravel's Capsule manager exposes a `setFetchMode()` method that writes configuration its connections do not read. Hypervel omits this ineffective connection-wide setter; use `Query\Builder::fetchUsing()` for each query that needs a custom row shape.
- `make:migration` omits Laravel's deprecated `--fullpath` option and obsolete Composer constructor dependency because migration creation no longer dumps autoload files.
- `Blueprint::dropForeign()` widens Laravel's method signature with an optional constraint name when columns are supplied, allowing explicitly named foreign keys to be dropped portably across SQLite and the server databases. Custom `Blueprint` subclasses that override this method must accept the optional second argument.
Expand Down
2 changes: 2 additions & 0 deletions src/docs/porting-from-laravel.md
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,8 @@ If your extension inspects transaction records or registers callbacks directly w

When a package constructs `DatabaseStore`, `DatabaseSessionHandler`, `DatabaseQueue`, or `DatabaseBatchRepository` directly, pass the database connection resolver and configured connection name instead of retaining a resolved connection. Framework-configured drivers already use this form.

Laravel's `ConnectionResolver` can be built from an array of connection instances, but Hypervel's `ConnectionResolver` is the pooled resolver created by the container. Where package tests build Laravel's resolver directly, mock `ConnectionResolverInterface` to inject existing connections, or use the Capsule manager for real configured connections.

Laravel's base `Connection` class exposes PDO methods. Hypervel's base `Connection` is driver-neutral, while its built-in SQL connections extend `PdoConnection`. Ported code that calls `getPdo`, `getReadPdo`, or another PDO-specific method should accept or narrow to `PdoConnection`. See [extending database connections](/docs/{{version}}/database#extending-database-connections) when porting a custom driver.

Custom base query builders overriding `newQuery`, `forNestedWhere`, or `cloneForPaginationCount` must declare `static` returns and preserve the concrete builder class. Keep `forSubQuery` separate: join subqueries return the parent query builder. See the [database extension guide](/docs/{{version}}/database#extending-database-connections) for these return contracts.
Expand Down
13 changes: 13 additions & 0 deletions src/docs/testbench.md
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,19 @@ PHP);

The `defineCacheRoutes` method writes a temporary route file into the runtime skeleton, runs `route:cache`, reloads the application, and removes the generated files during teardown.

Reloading creates a new application without running your test's `setUp` method again. If the reloaded application also needs setup from that method, register it with `afterApplicationRefreshed` before calling `parent::setUp()`. These callbacks run before the test case's traits are set up, and again each time the application is reloaded:

```php
use Hypervel\Support\Facades\Http;

protected function setUp(): void
{
$this->afterApplicationRefreshed(fn () => Http::preventStrayRequests());

parent::setUp();
}
```

If you need to define routes through the same temporary route-file mechanism without route caching, use `defineStashRoutes`:

```php
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ trait InteractsWithTestCaseLifecycle
*/
protected array $afterApplicationCreatedCallbacks = [];

/**
* The callbacks that should be run after the application is refreshed.
*/
protected array $afterApplicationRefreshedCallbacks = [];

/**
* The callbacks that should be run before the application is destroyed.
*/
Expand All @@ -54,6 +59,11 @@ trait InteractsWithTestCaseLifecycle
*/
protected bool $setUpHasRun = false;

/**
* Indicates if the application refreshed callbacks have started running.
*/
protected bool $afterApplicationRefreshedCallbacksStarted = false;

/**
* Set up the test environment.
*
Expand All @@ -74,6 +84,12 @@ protected function setUpTheTestEnvironment(): void
DatabaseConnectionResolver::resetCachedConnections();

$this->runInCoroutine(function () {
$this->afterApplicationRefreshedCallbacksStarted = true;

foreach ($this->afterApplicationRefreshedCallbacks as $callback) {
$callback();
}

$this->setUpTraits();

// Preserve transaction manager context for the test coroutine.
Expand Down Expand Up @@ -151,6 +167,8 @@ protected function tearDownTheTestEnvironment(): void
$this->beforeApplicationDestroyedCallbacks = [];
$this->callbackException = null;
$this->setUpHasRun = false;
// Refreshed callbacks are kept so a reloaded application runs them again.
$this->afterApplicationRefreshedCallbacksStarted = false;

try {
$this->flushState();
Expand All @@ -174,10 +192,8 @@ protected function flushState(): void

/**
* Boot the testing helper traits.
*
* @return array
*/
protected function setUpTraits()
protected function setUpTraits(): array
{
$uses = $this->traitsUsedByTest ??= class_uses_recursive(static::class);

Expand Down Expand Up @@ -286,10 +302,24 @@ public static function tearDownAfterClassUsingTestCase(): void
{
}

/**
* Register a callback to be run after the application is refreshed.
*
* The callbacks run before the test case's traits are set up, and again for each reloaded application.
*/
public function afterApplicationRefreshed(callable $callback): void
{
$this->afterApplicationRefreshedCallbacks[] = $callback;

if ($this->afterApplicationRefreshedCallbacksStarted) {
$callback();
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

/**
* Register a callback to be run after the application is created.
*/
public function afterApplicationCreated(callable $callback)
public function afterApplicationCreated(callable $callback): void
{
$this->afterApplicationCreatedCallbacks[] = $callback;

Expand All @@ -301,15 +331,15 @@ public function afterApplicationCreated(callable $callback)
/**
* Register a callback to be run before the application is destroyed.
*/
protected function beforeApplicationDestroyed(callable $callback)
protected function beforeApplicationDestroyed(callable $callback): void
{
$this->beforeApplicationDestroyedCallbacks[] = $callback;
}

/**
* Execute the application's pre-destruction callbacks.
*/
protected function callBeforeApplicationDestroyedCallbacks()
protected function callBeforeApplicationDestroyedCallbacks(): void
{
foreach ($this->beforeApplicationDestroyedCallbacks as $callback) {
try {
Expand Down
2 changes: 1 addition & 1 deletion src/mail/src/Mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ protected function parseView(array|Closure|string $view): array
// If the given view is an array with numeric keys, we will just assume that
// both a "pretty" and "plain" view were provided, so we will return this
// array as is, since it should contain both views with numerical keys.
if (isset($view[0])) {
if (array_key_exists(0, $view)) {
return [$view[0], $view[1], null];
}

Expand Down
3 changes: 2 additions & 1 deletion src/mail/src/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ protected function addAddresses(array|string $address, ?string $name, string $ty
return $this->ensureAddressIsSafe($address);
})->all();

$this->message->{"{$type}"}(...$addresses);
// Spread a list: string keys become named arguments, which makes any later positional address fail.
$this->message->{"{$type}"}(...array_values($addresses));
} else {
$this->message->{"add{$type}"}($this->createAddress($address, (string) $name));
}
Expand Down
4 changes: 2 additions & 2 deletions src/testbench/src/Concerns/Testing.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ trait Testing
*/
protected function reloadApplication(): void
{
$this->tearDown();
$this->setUp();
$this->tearDownTheTestbenchEnvironment();
$this->setUpTheTestbenchEnvironment();
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
}
}
34 changes: 34 additions & 0 deletions src/testbench/src/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ class TestCase extends BaseTestCase implements Contracts\TestCase
* Setup the test environment.
*/
protected function setUp(): void
{
$this->setUpTheTestbenchEnvironment();
}

/**
* Set up the Testbench environment.
*
* Application reloads call this directly, so the test case's own setUp() runs only once.
*
* @internal
*/
protected function setUpTheTestbenchEnvironment(): void
{
if ($this->withoutBootingFramework()) {
parent::setUp();
Expand Down Expand Up @@ -231,6 +243,18 @@ protected function refreshApplication(): void
* Clean up the testing environment before the next test.
*/
protected function tearDown(): void
{
$this->tearDownTheTestbenchEnvironment();
}

/**
* Clean up the Testbench environment.
*
* Application reloads call this directly, so the test case's own tearDown() runs only once.
*
* @internal
*/
protected function tearDownTheTestbenchEnvironment(): void
{
if ($this->withoutBootingFramework()) {
parent::tearDown();
Expand Down Expand Up @@ -290,6 +314,16 @@ protected function tearDown(): void
}
}

/**
* Register a callback to be run before the application is destroyed.
*
* Callbacks run newest first, so later setup is cleaned up before the setup it depends on.
*/
public function beforeApplicationDestroyed(callable $callback): void
{
array_unshift($this->beforeApplicationDestroyedCallbacks, $callback);
}

/**
* Prepare the testing environment before the running the test case.
*/
Expand Down
72 changes: 34 additions & 38 deletions src/validation/src/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,10 @@ class Validator implements ValidatorContract

/**
* Attributes that should be excluded from the validated data.
*/
protected array $excludeAttributes = [];

/**
* Active exclusions for exact-base compiled execution.
*
* Subclasses keep Laravel's protected list because their passes() methods
* may reset that list directly.
*
* @var array<string, true>
* @var array<string, string>
*/
private array $activeExclusions = [];
protected array $excludeAttributes = [];

/**
* The message bag instance.
Expand Down Expand Up @@ -434,7 +426,6 @@ public function passes(): bool
$this->messages = new MessageBag;
[$this->distinctValues, $this->failedRules, $this->excludeAttributes] = [[], [], []];
$this->distinctValueCounts = [];
$this->activeExclusions = [];
$this->originalPresenceVerifier = null;
$this->parsedTables = [];

Expand Down Expand Up @@ -982,28 +973,17 @@ public function whenFails(callable $callback, ?callable $default = null): mixed
*/
protected function shouldBeExcluded(string $attribute): bool
{
if (static::class === self::class) {
return isset($this->activeExclusions[$attribute])
|| ($this->activeExclusions !== []
&& $this->hasAttributeAncestorInSet($attribute, $this->activeExclusions));
}

// Keep the subclass fallback as a loop to avoid an extra callback per exclusion.
foreach ($this->excludeAttributes as $excludeAttribute) {
if ($attribute === $excludeAttribute
|| Str::startsWith($attribute, $excludeAttribute . '.')
) {
return true;
}
}

return false;
// Unlike Laravel's explode() loop, this skips the ancestor walk when nothing
// is excluded and avoids splitting every attribute into a segment array.
return isset($this->excludeAttributes[$attribute])
|| ($this->excludeAttributes !== []
&& $this->hasAttributeAncestorInSet($attribute, $this->excludeAttributes));
}

/**
* Determine if any strict ancestor of an attribute belongs to a set.
*
* @param array<string, true> $attributes
* @param array<string, string|true> $attributes
*/
private function hasAttributeAncestorInSet(string $attribute, array $attributes): bool
{
Expand Down Expand Up @@ -1092,6 +1072,8 @@ public function validated(): array

$missingValue = new stdClass;

$parentKeys = $this->excludeUnvalidatedArrayKeys ? $this->parentRuleKeys() : [];

foreach ($this->getRules() as $key => $rules) {
$key = (string) $key;

Expand All @@ -1101,7 +1083,7 @@ public function validated(): array
$this->excludeUnvalidatedArrayKeys
&& (in_array('array', $rules) || in_array('list', $rules))
&& $value !== null
&& ! empty(preg_grep('/^' . preg_quote($key, '/') . '\.+/', array_keys($this->getRules())))
&& isset($parentKeys[$key])
) {
continue;
}
Expand All @@ -1114,6 +1096,28 @@ public function validated(): array
return $this->replacePlaceholders($results);
}

/**
* Get the rule keys that have nested rules beneath them.
*
* @return array<string, true>
*/
protected function parentRuleKeys(): array
{
$parentKeys = [];

foreach (array_keys($this->getRules()) as $key) {
$key = (string) $key;

while (str_contains($key, '.')) {
$key = Str::beforeLast($key, '.');

$parentKeys[$key] = true;
}
}

return $parentKeys;
}

/**
* Validate a given attribute against a rule.
*/
Expand Down Expand Up @@ -1464,15 +1468,7 @@ public function addFailure(string $attribute, string $rule, array $parameters =
*/
protected function excludeAttribute(string $attribute): void
{
if (static::class === self::class) {
$this->activeExclusions[$attribute] = true;

return;
}

$this->excludeAttributes[] = $attribute;

$this->excludeAttributes = array_unique($this->excludeAttributes);
$this->excludeAttributes[$attribute] = $attribute;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/view/src/DynamicComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ protected function compileSlots(array $slots): string
{
return (new Collection($slots))
->reject(fn ($slot, $name) => $name === '__default')
->map(fn ($slot, $name) => '<x-slot name="' . $name . '" ' . ((string) $slot->attributes) . '>{{ $' . $name . ' }}</x-slot>')
->map(fn ($slot, $name) => '<x-slot name="' . $name . '" :attributes="$' . $name . '->attributes">{{ $' . $name . ' }}</x-slot>')
->implode(PHP_EOL);
}

Expand Down
Loading
Loading