Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8990777
Clarify class imports for new and ported code
binaryfire Sep 5, 2026
29e238d
Document database query listener callback types and lifetime
binaryfire Sep 5, 2026
732500a
Exempt test methods from required title docblocks
binaryfire Sep 5, 2026
77aff7f
Complete queued notification routing test parity
binaryfire Sep 5, 2026
91bc8ea
Merge 0.4 into laravel-parity
binaryfire Sep 5, 2026
494aeff
Run complete Queue and Cache integration suites against Redis
binaryfire Sep 5, 2026
e21a410
Complete Laravel file validation parity for Symfony files and uploads
binaryfire Sep 5, 2026
fffbf3b
Port queue worker stop output from Laravel
binaryfire Sep 6, 2026
38a901f
Port context propagation across explicit process boundaries
binaryfire Sep 6, 2026
d18725f
Port Laravel collection filtering updates
binaryfire Sep 6, 2026
8d202fa
Fix Composer package uninstall event dispatch
binaryfire Sep 6, 2026
25dc2ea
Clarify PHPUnit exception assertion cleanup
binaryfire Sep 6, 2026
f7f5dd9
Port queue totals and complete Redis bulk parity
binaryfire Sep 6, 2026
ada8ba2
Port application configuration cache memoization
binaryfire Sep 6, 2026
1732de0
Honor configured application paths in app_path
binaryfire Sep 6, 2026
5fc7665
Port factory insertion fixes and preserve binary bulk bindings
binaryfire Sep 6, 2026
3454fa0
Port cached-route detection memoization and test parity
binaryfire Sep 6, 2026
37a3576
Complete ucwords separator parity and handle empty separators
binaryfire Sep 6, 2026
0110180
Complete string helper parity and correct Unicode wrapping
binaryfire Sep 6, 2026
7e82102
Merge branch '0.4' into laravel-parity
binaryfire Sep 6, 2026
513669f
Complete null-safe query comparison parity across supported databases
binaryfire Sep 6, 2026
79c0351
Fix Redis scan patterns and consume startup context transport
binaryfire Sep 6, 2026
b56af4b
Test gRPC bootstrap against a real cached HTTP route
binaryfire Sep 6, 2026
da0579d
Clarify the SafeScan logical pattern parameter contract
binaryfire Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions .github/workflows/redis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,11 @@ jobs:
run: |
vendor/bin/paratest --max-processes=15 tests/Integration/Auth/Redis
vendor/bin/paratest --max-processes=15 tests/Integration/Broadcasting/Redis
vendor/bin/paratest --max-processes=15 tests/Integration/Cache/Redis
CACHE_STORE=redis vendor/bin/paratest --max-processes=15 tests/Integration/Cache
vendor/bin/paratest --max-processes=15 tests/Integration/Horizon
vendor/bin/paratest --max-processes=15 tests/Integration/Http/Redis
vendor/bin/paratest --max-processes=15 tests/Integration/OpenTelemetry/Redis
vendor/bin/paratest --max-processes=15 tests/Integration/Queue/Redis
QUEUE_CONNECTION=redis vendor/bin/phpunit --no-progress tests/Integration/Queue/JobChainingTest.php
QUEUE_CONNECTION=redis vendor/bin/phpunit --no-progress tests/Integration/Queue/JobDispatchingTest.php
QUEUE_CONNECTION=redis vendor/bin/paratest --max-processes=15 tests/Integration/Queue
vendor/bin/paratest --max-processes=15 tests/Integration/RateLimiter/Redis
vendor/bin/paratest --max-processes=15 tests/Integration/Redis
vendor/bin/paratest --max-processes=15 tests/Integration/Session/Redis
Expand Down Expand Up @@ -225,13 +223,11 @@ jobs:
run: |
vendor/bin/phpunit --no-progress tests/Integration/Auth/Redis
vendor/bin/phpunit --no-progress tests/Integration/Broadcasting/Redis
vendor/bin/phpunit --no-progress tests/Integration/Cache/Redis
CACHE_STORE=redis vendor/bin/phpunit --no-progress tests/Integration/Cache
vendor/bin/phpunit --no-progress tests/Integration/Horizon
vendor/bin/phpunit --no-progress tests/Integration/Http/Redis
vendor/bin/phpunit --no-progress tests/Integration/OpenTelemetry/Redis
vendor/bin/phpunit --no-progress tests/Integration/Queue/Redis
QUEUE_CONNECTION=redis vendor/bin/phpunit --no-progress tests/Integration/Queue/JobChainingTest.php
QUEUE_CONNECTION=redis vendor/bin/phpunit --no-progress tests/Integration/Queue/JobDispatchingTest.php
QUEUE_CONNECTION=redis vendor/bin/phpunit --no-progress tests/Integration/Queue
vendor/bin/phpunit --no-progress tests/Integration/RateLimiter/Redis
vendor/bin/phpunit --no-progress tests/Integration/Session/Redis
# This explicit list contains the topology-neutral Redis tests; the remaining files require standalone-only behavior.
Expand Down Expand Up @@ -306,13 +302,11 @@ jobs:
run: |
vendor/bin/paratest --max-processes=15 tests/Integration/Auth/Redis
vendor/bin/paratest --max-processes=15 tests/Integration/Broadcasting/Redis
vendor/bin/paratest --max-processes=15 tests/Integration/Cache/Redis
CACHE_STORE=redis vendor/bin/paratest --max-processes=15 tests/Integration/Cache
vendor/bin/paratest --max-processes=15 tests/Integration/Horizon
vendor/bin/paratest --max-processes=15 tests/Integration/Http/Redis
vendor/bin/paratest --max-processes=15 tests/Integration/OpenTelemetry/Redis
vendor/bin/paratest --max-processes=15 tests/Integration/Queue/Redis
QUEUE_CONNECTION=redis vendor/bin/phpunit --no-progress tests/Integration/Queue/JobChainingTest.php
QUEUE_CONNECTION=redis vendor/bin/phpunit --no-progress tests/Integration/Queue/JobDispatchingTest.php
QUEUE_CONNECTION=redis vendor/bin/paratest --max-processes=15 tests/Integration/Queue
vendor/bin/paratest --max-processes=15 tests/Integration/RateLimiter/Redis
vendor/bin/paratest --max-processes=15 tests/Integration/Redis
vendor/bin/phpunit --no-progress tests/Integration/Reverb/ClearStateCommandTest.php
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@
"ably/ably-php": "^1.0",
"algolia/algoliasearch-client-php": "^4.0",
"brianium/paratest": "^7.24",
"composer/composer": "^2.10.3",
"composer/semver": "^3.4",
"fakerphp/faker": "^1.24",
"friendsofphp/php-cs-fixer": "^3.57.2",
Expand Down
2 changes: 1 addition & 1 deletion docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

## Testing

- Replace PHPUnit 13's deprecated `expectExceptionMessage()` calls across the test suite. Use `expectExceptionMessageIs()` for complete messages and `expectExceptionMessageIsOrContains()` for fragments, auditing each assertion's intent and running its owning test file as it is changed.
- Replace PHPUnit 13's [soft-deprecated `expectExceptionMessage()`](https://github.com/sebastianbergmann/phpunit/issues/6560) calls across the test suite. Preserve intended matching semantics: use `expectExceptionObject()` for combined class/message/code expectations, `expectExceptionMessageIs()` for exact messages, and `expectExceptionMessageIsOrContains()` for substring matching. Audit each assertion's intent and run its owning test file as it is changed.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## HTTP Server

Expand Down
1 change: 0 additions & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ parameters:
- %currentWorkingDirectory%/src/*/node_modules/*
- %currentWorkingDirectory%/src/*/resources/views/*
- %currentWorkingDirectory%/src/*/publish/*
- %currentWorkingDirectory%/src/foundation/src/ComposerScripts.php
ignoreErrors:
# CreatesApplication is an open trait consumed by both PHPUnit test cases and the
# standalone Testbench application. PHPStan reports this shared capability guard
Expand Down
6 changes: 4 additions & 2 deletions src/cache/src/Redis/Operations/AllTag/GetEntries.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ public function execute(array $tagIds): LazyCollection

do {
$entries = $context->withConnection(
function (RedisConnection $connection) use ($prefix, $tagId, &$cursor) {
return $connection->zscan($prefix . $tagId, $cursor, '*', 1000);
function (RedisConnection $connection) use ($prefix, $tagId, &$cursor): mixed {
return $connection->withoutScanPrefix(function () use ($connection, $prefix, $tagId, &$cursor): mixed {
return $connection->zscan($prefix . $tagId, $cursor, '*', 1000);
});
}
);

Expand Down
6 changes: 4 additions & 2 deletions src/cache/src/Redis/Operations/AllTag/Prune.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ private function removeOrphanedEntries(
$iterator = PhpRedis::initialScanCursor();

do {
// ZSCAN returns [member => score, ...] array
$members = $connection->zScan($tagKey, $iterator, '*', $scanCount);
// Tag members omit OPT_PREFIX, so scan every member without prefixing the pattern.
$members = $connection->withoutScanPrefix(function () use ($connection, $tagKey, &$iterator, $scanCount): mixed {
return $connection->zScan($tagKey, $iterator, '*', $scanCount);
});

if ($members === false || ! is_array($members)) {
break;
Expand Down
6 changes: 4 additions & 2 deletions src/cache/src/Redis/Operations/AnyTag/GetTaggedKeys.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ private function hscanGenerator(string $tagKey, int $count): Generator
do {
// Acquire connection just for this HSCAN batch
$fields = $this->context->withConnection(
function (RedisConnection $connection) use ($tagKey, &$iterator, $count) {
return $connection->hscan($tagKey, $iterator, null, $count);
function (RedisConnection $connection) use ($tagKey, &$iterator, $count): mixed {
return $connection->withoutScanPrefix(function () use ($connection, $tagKey, &$iterator, $count): mixed {
return $connection->hscan($tagKey, $iterator, null, $count);
});
}
);

Expand Down
12 changes: 8 additions & 4 deletions src/cache/src/Redis/Operations/AnyTag/Prune.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,10 @@ private function cleanupTagHashUsingLua(
$iterator = PhpRedis::initialScanCursor();

do {
// HSCAN returns [field => value, ...] array
$fields = $connection->hScan($tagHash, $iterator, '*', $scanCount);
// Tag fields omit OPT_PREFIX, so scan every field without prefixing the pattern.
$fields = $connection->withoutScanPrefix(function () use ($connection, $tagHash, &$iterator, $scanCount): mixed {
return $connection->hScan($tagHash, $iterator, '*', $scanCount);
});

if ($fields === false || ! is_array($fields)) {
break;
Expand Down Expand Up @@ -230,8 +232,10 @@ private function cleanupTagHashCluster(
$iterator = PhpRedis::initialScanCursor();

do {
// HSCAN returns [field => value, ...] array
$fields = $connection->hScan($tagHash, $iterator, '*', $scanCount);
// Tag fields omit OPT_PREFIX, so scan every field without prefixing the pattern.
$fields = $connection->withoutScanPrefix(function () use ($connection, $tagHash, &$iterator, $scanCount): mixed {
return $connection->hScan($tagHash, $iterator, '*', $scanCount);
});

if ($fields === false || ! is_array($fields)) {
break;
Expand Down
1 change: 1 addition & 0 deletions src/concurrency/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"hypervel/context": "^0.4",
"hypervel/contracts": "^0.4",
"hypervel/coroutine": "^0.4",
"hypervel/log": "^0.4",
"hypervel/process": "^0.4",
"hypervel/support": "^0.4",
"nesbot/carbon": "^3.13.1",
Expand Down
5 changes: 5 additions & 0 deletions src/concurrency/src/ProcessDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Hypervel\Process\Pool;
use Hypervel\Support\Arr;
use Hypervel\Support\Defer\DeferredCallback;
use Hypervel\Support\Facades\Context;
use Laravel\SerializableClosure\SerializableClosure;

use function Hypervel\Support\defer;
Expand All @@ -37,6 +38,8 @@ public function run(Closure|array $tasks, CarbonInterval|int|null $timeout = nul
$results = $this->processFactory->pool(function (Pool $pool) use ($tasks, $command, $timeout) {
foreach (Arr::wrap($tasks) as $key => $task) {
$process = $pool->as((string) $key)->path(base_path())->env([
/* @phpstan-ignore staticMethod.notFound */
'__HYPERVEL_CONTEXT' => base64_encode(serialize(Context::dehydrate())),
'HYPERVEL_INVOKABLE_CLOSURE' => base64_encode(
serialize(new SerializableClosure($task))
),
Expand Down Expand Up @@ -67,6 +70,8 @@ public function defer(Closure|array $tasks): DeferredCallback
return defer(function () use ($tasks, $command) {
foreach (Arr::wrap($tasks) as $task) {
$this->processFactory->path(base_path())->env([
/* @phpstan-ignore staticMethod.notFound */
'__HYPERVEL_CONTEXT' => base64_encode(serialize(Context::dehydrate())),
'HYPERVEL_INVOKABLE_CLOSURE' => base64_encode(
serialize(new SerializableClosure($task))
),
Expand Down
33 changes: 33 additions & 0 deletions src/console/src/ConsoleServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
use Hypervel\Console\Commands\ScheduleResumeCommand;
use Hypervel\Console\Commands\ScheduleRunCommand;
use Hypervel\Console\Commands\ScheduleTestCommand;
use Hypervel\Console\Events\BeforeHandle;
use Hypervel\Contracts\Events\Dispatcher;
use Hypervel\Log\Context\Repository;
use Hypervel\Support\Env;
use Hypervel\Support\ServiceProvider;

class ConsoleServiceProvider extends ServiceProvider
Expand All @@ -30,4 +34,33 @@ public function register(): void
ScheduleTestCommand::class,
]);
}

/**
* Bootstrap the service provider.
*/
public function boot(Dispatcher $events): void
{
if (! $this->app->runningInConsole()
|| ($encoded = Env::get('__HYPERVEL_CONTEXT')) === null) {
return;
}

// Consume startup transport at boot so child processes cannot inherit stale context.
Env::deleteMany(['__HYPERVEL_CONTEXT']);
// The native environment also needs clearing when Env's putenv adapter is disabled.
putenv('__HYPERVEL_CONTEXT');

if (is_string($encoded)
&& is_array($context = unserialize(base64_decode($encoded, true), ['allowed_classes' => false]))) {
$events->listen(BeforeHandle::class, static function () use (&$context): void {
if ($context !== null) {
// Hydration callbacks may invoke another command, so claim the startup context first.
$payload = $context;
$context = null;

Repository::getInstance()->hydrate($payload);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
});
}
}
}
6 changes: 5 additions & 1 deletion src/console/src/Scheduling/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Hypervel\Contracts\Foundation\Application as ApplicationContract;
use Hypervel\Contracts\Mail\Mailer;
use Hypervel\Filesystem\Filesystem;
use Hypervel\Log\Context\Repository;
use Hypervel\Support\Arr;
use Hypervel\Support\Facades\Date;
use Hypervel\Support\Stringable;
Expand Down Expand Up @@ -265,10 +266,13 @@ protected function execute(Container $container): int
*/
protected function runProcess(Container $container): int
{
$context = base64_encode(serialize(Repository::getInstance()->dehydrate()));

/** @var \Hypervel\Contracts\Foundation\Application $container */
$process = Process::fromShellCommandline(
$this->command,
$container->basePath()
$container->basePath(),
['__HYPERVEL_CONTEXT' => $context]
);

CoroutineContext::set($this->processContextKey(), $process);
Expand Down
7 changes: 6 additions & 1 deletion src/database/src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -1027,10 +1027,15 @@ public function getErrorCount(): int

/**
* Register a database query listener with the connection.
*
* Boot-only. Registers a listener on the worker-global event dispatcher;
* per-request registration persists and affects subsequent requests.
*
* @param Closure(QueryExecuted): mixed $callback
*/
public function listen(Closure $callback): void
{
$this->events?->listen(Events\QueryExecuted::class, $callback);
$this->events?->listen(QueryExecuted::class, $callback);
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/database/src/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -482,10 +482,10 @@ public function fillForInsert(array $values): array

$this->model->unguarded(function () use (&$values) {
foreach ($values as $key => $rowValues) {
$values[$key] = tap(
$this->newModelInstance($rowValues),
fn ($model) => $model->setUniqueIds()
)->getAttributes();
$model = $this->newModelInstance($rowValues);
$model->setUniqueIds();

$values[$key] = $model->prepareBinaryAttributesForDatabase($model->getAttributes());
}
});

Expand Down
2 changes: 1 addition & 1 deletion src/database/src/Eloquent/Concerns/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -1920,7 +1920,7 @@ private function isBinaryCast(?string $cast): bool
* @param array<string, mixed> $attributes
* @return array<string, mixed>
*/
protected function prepareBinaryAttributesForDatabase(array $attributes): array
public function prepareBinaryAttributesForDatabase(array $attributes): array
{
$casts = $this->getCasts();

Expand Down
33 changes: 26 additions & 7 deletions src/database/src/Eloquent/Factories/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ public function lazy(array $attributes = [], ?Model $parent = null): Closure
/**
* Set the connection name on the results and store them.
*
* @param \Hypervel\Support\Collection<int, \Hypervel\Database\Eloquent\Model> $results
* @param \Hypervel\Support\Collection<int, TModel> $results
*/
protected function store(Collection $results): void
{
Expand All @@ -346,6 +346,8 @@ protected function store(Collection $results): void

/**
* Create the children for the given model.
*
* @param TModel $model
*/
protected function createChildren(Model $model): void
{
Expand Down Expand Up @@ -444,6 +446,10 @@ public function insert(array $attributes = [], ?Model $parent = null): void
? $made
: $this->newModel()->newCollection([$made]);

if ($madeCollection->isEmpty()) {
return;
}

$model = $madeCollection->first();

if (isset($this->connection)) {
Expand All @@ -452,12 +458,25 @@ public function insert(array $attributes = [], ?Model $parent = null): void

$query = $model->newQueryWithoutScopes();

$query->fillAndInsert(
$madeCollection->withoutAppends()
->setHidden([])
->map(static fn (Model $model) => $model->attributesToArray())
->all()
);
$timestamps = [];

if ($model->usesTimestamps()) {
$timestamp = $model->freshTimestampString();

foreach (array_filter([$model->getCreatedAtColumn(), $model->getUpdatedAtColumn()]) as $column) {
$timestamps[$column] = $timestamp;
}
}

// These models have already applied their casts and mutators. Serializing or
// filling them again can drop attributes or transform stored values twice.
$values = $madeCollection->map(static function (Model $model) use ($timestamps): array {
$model->setUniqueIds();

return array_merge($timestamps, $model->prepareBinaryAttributesForDatabase($model->getAttributes()));
})->all();

$query->insert($values);
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/database/src/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,11 @@ public function where(Closure|self|EloquentBuilder|Relation|ExpressionContract|a
$type = 'Bitwise';
}

// JSON booleans need their driver's casts before applying null-safe equality.
if ($operator === '<=>' && $type !== 'JsonBoolean') {
$type = 'NullSafeEquals';
}

// Now that we are working with just a simple query we can put the elements
// in our array and add the query binding to our array of bindings that
// will be bound to each SQL statements when it is finally executed.
Expand Down Expand Up @@ -1143,6 +1148,10 @@ public function orWhereNotLike(ExpressionContract|string $column, string $value,
*/
public function whereNullSafeEquals(ExpressionContract|string $column, mixed $value, string $boolean = 'and'): static
{
if (is_bool($value) && is_string($column) && str_contains($column, '->')) {
return $this->where($column, '<=>', $value, $boolean);
}

$type = 'NullSafeEquals';

$this->wheres[] = compact('type', 'column', 'value', 'boolean');
Expand Down
14 changes: 14 additions & 0 deletions src/database/src/Query/Grammars/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Hypervel\Database\Concerns\CompilesJsonPaths;
use Hypervel\Database\Grammar as BaseGrammar;
use Hypervel\Database\Query\Builder;
use Hypervel\Database\Query\Expression as QueryExpression;
use Hypervel\Database\Query\JoinClause;
use Hypervel\Database\Query\JoinLateralClause;
use Hypervel\Support\Arr;
Expand Down Expand Up @@ -512,6 +513,12 @@ protected function whereSub(Builder $query, array $where): string
$subquery = $where['query'];
$select = $subquery->getGrammar()->compileSelectQuery($subquery);

if ($where['operator'] === '<=>') {
$where['value'] = new QueryExpression("({$select})");

return $this->whereNullSafeEquals($query, $where);
}

return $this->wrap($where['column']) . ' ' . $where['operator'] . " ({$select})";
}

Expand Down Expand Up @@ -558,6 +565,13 @@ protected function whereJsonBoolean(Builder $query, array $where): string
$this->parameter($where['value'])
);

if ($where['operator'] === '<=>') {
$where['column'] = new QueryExpression($column);
$where['value'] = new QueryExpression($value);

return $this->whereNullSafeEquals($query, $where);
}

return $column . ' ' . $where['operator'] . ' ' . $value;
}

Expand Down
Loading