⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content

Conversation

@binaryfire
Copy link
Contributor

Hi @albertcht. This isn't ready yet but I'm opening it as a draft so we can begin discussions and code reviews. The goal of this PR is to refactor Hypervel to be a fully standalone framework that is as close to 1:1 parity with Laravel as possible.

Why one large PR

Sorry about the size of this PR. I tried spreading things across multiple branches but it made my work a lot more difficult. This is effectively a framework refactor - the database package is tightly coupled to many other packages (collections, pagination, pool) as well as several support classes, so all these things need to be updated together. Splitting it across branches would mean each branch needs multiple temporary workarounds + would have failing tests until merged together, making review and CI impractical.

A single large, reviewable PR is less risky than a stack of dependent branches that can't pass CI independently.


Reasons for the refactor

1. Outdated Hyperf packages

It's been difficult to migrate existing Laravel projects to Hypervel because Hyperf's database packages are quite outdated. There are almost 100 missing methods, missing traits, it doesn't support nested transactions, there are old Laravel bugs which haven't been fixed (eg. JSON indices aren't handled correctly), coroutine safety issues (eg. model unguard(), withoutTouching()). Other packages like pagination, collections and support are outdated too. Stringable was missing a bunch of methods and traits, for example. There are just too many to PR to Hyperf at this point.

2. Faster framework development

We need to be able to move quickly and waiting for Hyperf maintainers to merge things adds a lot of friction to framework development. Decoupling means we don't need to work around things like PHP 8.4 compatibility while waiting for it to be added upstream. Hyperf's testing package uses PHPUnit 10 so we can't update to PHPUnit 13 (and Pest 4 in the skeleton) when it releases in a couple of weeks. v13 has the fix that allows RunTestsInCoroutine to work with newer PHPUnit versions. There are lots of examples like this.

3. Parity with Laravel

We need to avoid the same drift from Laravel that's happened with Hyperf since 2019. If we're not proactive with regularly merging Laravel updates every week we'll end up in the same situation. Having a 1:1 directory and code structure to Laravel whenever possible will make this much easier. Especially when using AI tools.

Most importantly, we need to make it easier for Laravel developers to use and contribute to the framework. That means following the same APIs and directory structures and only modifying code when there's a good reason to (coroutine safety, performance, type modernisation etc).

Right now the Hypervel codebase is confusing for both Laravel developers and AI tools:

  • Some classes use Hyperf classes directly, some extend them, some replace them. You need to check multiple places to see what methods are available
  • Some Hyperf methods have old (2019) Laravel signatures while some overridden ones have new ones
  • The classes are in different locations to Laravel (eg. there's no hypervel/contracts package, the Hyperf database code is split across 3 packages, the Hyperf pagination package is hyperf/paginator and not hyperf/pagination)
  • The tests dir structure and class names are different, making it hard to know what tests are missing when comparing them to Laravel's tests dir
  • There are big differences in the API (eg. static::registerCallback('creating') vs static::creating())
  • The mix of Hyperf ConfigProvider and Laravel ServiceProvider patterns across different packages is confusing for anyone who doesn't know Hyperf
  • There are big functional differences eg. no nested database transactions

This makes it difficult for Laravel developers to port over apps and to contribute to the framework.

4. AI

The above issues mean that AI needs a lot of guidance to understand the Hypervel codebase and generate Hypervel boilerplate. A few examples:

  • Models have trained extensively on Laravel code and expect things to have the same API. Generated boilerplate almost always contains incompatible Laravel-style code which means you have to constantly interrupt and guide them to the Hypervel-specific solutions.
  • Models get confused when they have to check both Hypervel and Hyperf dependencies. They start by searching for files in the same locations as Laravel (eg. hypervel/contracts for contracts) and then have to spend a lot of time grepping for things to find them.
  • The inheritance chain causes major problems. Models often search Hypervel classes for methods and won't remember to search the parent Hyperf classes as well.

And so on... This greatly limits the effectiveness of building Hypervel apps with AI. Unfortunately MCP docs servers and CLAUDE.md rules don't solve all these problems - LLMs aren't great at following instructions well and the sheer volume of Laravel data they've trained on means they always default to Laravel-style code. The only solution is 1:1 parity. Small improvements such as adding native type hints are fine - models can solve that kind of thing quickly from exception messages.


What changed so far

New packages

Package Purpose
hypervel/database Full illuminate/database port
hypervel/collections Full illuminate/collections port
hypervel/pagination Full illuminate/pagination port
hypervel/contracts Centralised cross-cutting contracts (same as illuminate/contracts)
hypervel/pool Connection pooling (internalised from hyperf/pool)
hypervel/macroable Moved Macroable to a separate package for Laravel parity

Removed Hyperf dependencies so far

  • hyperf/database
  • hyperf/database-pgsql
  • hyperf/database-sqlite
  • hyperf/db-connection
  • hyperf/collection
  • hyperf/stringable
  • hyperf/tappable
  • hyperf/macroable
  • hyperf/codec

Database package

The big task was porting the database package, making it coroutine safe, implementing performance improvements like static caching and modernising the types.

  • Ported from Laravel 12
  • Added 50+ missing methods: whereLike, whereNot, groupLimit, rawValue, soleValue, JSON operations, etc.
  • Full Schema builder parity including schema states
  • Complete migration system (commands are still using Hyperf generators for now)
  • Both Hyperf and Hypervel previously used global PHPStan ignores on database classes. I removed these and fixed the underlying issues - only legitimate "magic" things are ignored now.

Collections package

  • Full Laravel parity across all methods
  • Modernised with PHP 8.2+ native types
  • Full LazyCollection support
  • Proper generics for static analysis

Contracts package

  • Centralised cross-cutting interfaces (like illuminate/contracts)
  • Clean dependency boundaries - packages depend on contracts, not implementations
  • Enables proper dependency inversion across the framework

Support package

  • Removed hyperf/tappable, hyperf/stringable, hyperf/macroable, hyperf/codec dependencies
  • Freshly ported Str, Env and helper classes from Laravel
  • Consistent use of Hypervel\Context wrappers (will be porting hyperf/context soon)
  • Fixed some existing bugs (eg. Number::useCurrency() wasn't actually setting the currency)

Coroutine safety

  • withoutEvents(), withoutBroadcasting(), withoutTouching() now use Context instead of static properties
  • Added UnsetContextInTaskWorkerListener to clear database context in task workers
  • Added Connection::resetForPool() to prevent state leaks between coroutines
  • Made DatabaseTransactionsManager coroutine-safe
  • Comprehensive tests verify isolation

Benefits

  1. Laravel 1:1 API parity - code from Laravel docs works directly
  2. AI compatibility - AI assistants generate working Hypervel code
  3. Easier contributions - Laravel devs can contribute without learning Hyperf
  4. Reduced maintenance - fewer external dependencies to track and update
  5. Modern PHP - native types throughout, PHP 8.2+ patterns
  6. Better static analysis - proper generics and type hints
  7. Coroutine safety - verified isolation with comprehensive tests

Testing status so far

  • PHPStan passes at level 5
  • All existing tests pass
  • Coroutine isolation tests verify safety

What's left (WIP)

  • Port the remaining Hyperf dependencies
  • Port the full Laravel and Hyperf test suites
  • Documentation updates
  • Your feedback on architectural decisions

The refactor process

Hyperf's Swoole packages like pool, coroutine, context and http-server haven't changed in many years so porting these is straightforward. A lot of the code can be simplified since we don't need SWOW support. And we can still support the ecosystem by contributing any improvements we make back to Hyperf in separate PRs.

Eventually I'll refactor the bigger pieces like the container (contextual binding would be nice!) and the config system (completely drop ConfigProvider and move entirely to service providers). But those will be future PRs. For now the main refactors are the database layer, collections and support classes + the simple Hyperf packages. I'll just port the container and config packages as-is for now.


Let me know if you have any feedback, questions or suggestions. I'm happy to make any changes you want. I suggest we just work through this gradually, as an ongoing task over the next month or so. I'll continue working in this branch and ping you each time I add something new.

@binaryfire binaryfire marked this pull request as draft January 26, 2026 03:51
@binaryfire
Copy link
Contributor Author

binaryfire commented Jan 26, 2026

@albertcht To illustrate how much easier it will be to keep Hypervel in sync with Laravel after this refactor, I asked Claude how long it would take to merge laravel/framework#58461 (as an example) into this branch. This is what it said:

Once the Laravel database tests are ported to Hypervel, incorporating Laravel PRs becomes straightforward:

  1. Fetch the PR diff with `gh pr diff <number> --repo laravel/framework`
  2. Apply the same changes to the equivalent Hypervel files (usually just namespace changes from `Illuminate` → `Hypervel`)
  3. Run the ported tests to verify the behavior matches

  The main work is already done - the logic is identical, just namespaced differently.

  The PR I just looked at would have taken ~5 minutes to apply:
  
  - Add `selectExpression()` method
  - Add two `elseif` checks in `select()` and `addSelect()`
  - Add a type assertion in the types file
  - Run existing tests

So just 5-10 minutes of work with the help of AI tooling! Merging individual PRs is inefficient - merging releases would be better. I can set up a Discord channel where new releases are automatically posted via webhooks. Maybe someone in your team can be responsible for monitoring that channel's notifications and merging updates ever week or 2? I'll only be 1-2 hours of work once the codebases are 1:1.

We should be diligent about staying on top of merging updates. Otherwise we'll end up in in the same as Hyperf - i.e. the codebase being completely out of date with the current Laravel API.

@albertcht albertcht added the breaking-change Breaking changes label Jan 27, 2026
- Create hypervel/conditionable package with Conditionable trait and
  HigherOrderWhenProxy class (moved from Support)
- Create hypervel/reflection package with ReflectsClosures trait and
  lazy()/proxy() helper functions for PHP 8.4 lazy objects
- Move InteractsWithTime from Support/Traits to Support (matching Laravel)
- Update ReflectsClosures with missing closureReturnTypes() method
- Update all namespace references across the codebase
Laravel's conditionable and reflection packages use Illuminate\Support
namespace despite being in separate package directories. This commit
fixes the namespaces to use Hypervel\Support\* instead of package-specific
namespaces, matching Laravel's approach.
Files ported/updated:
- DatabaseEloquentModelTest.php (188/211 tests passing - 89%)
- DatabaseEloquentIrregularPluralTest.php (3 tests)
- DatabaseEloquentLocalScopesTest.php (6 tests)
- Enums.php - fixed toArray() return type
- stubs/TestCast.php - fixed CastsAttributes interface import

DatabaseEloquentModelTest remaining issues (22 errors, 1 failure):
- Mock type mismatches for newQueryWithoutScopes() returning stdClass
- testGlobalGuarded expects MassAssignmentException not thrown

Key fixes:
- Changed base class to Testbench\TestCase for container access
- Fixed Dispatcher import (Event vs Events namespace)
- Updated namespace references from Illuminate to Hypervel
- Added typed properties to model stubs
Laravel's setAttribute() accepts both string and int keys without type hints.
Hypervel's strict typing broke this for numeric keys.

Solution: Accept string|int at the entry point, but early-return for int keys
since they cannot have mutators or casts. This keeps downstream string utilities
(Str::studly, hasSetMutator, etc.) correctly typed for strings while supporting
the numeric key use case.
- Replace class-based events with string events ("eloquent.{event}: {model}")
- Add registerObserver() and resolveObserverClassName() methods from Laravel
- Update observe() to use registerObserver() instead of ModelListener
- Update registerModelEvent() to dispatch string events directly
- Update fireModelEvent() to fire string events (preserve eventsDisabled check)
- Update flushEventListeners() to clear from dispatcher only
- Remove $modelEventClasses, getModelEventClass(), getModelListener()
- Remove unused Event class imports and ModelListener dependency
- Preserve Swoole-specific: withoutEvents(), eventsDisabled() using Context
Update getObservers() to query the event dispatcher's raw listeners directly
for eloquent.* events, matching how Laravel retrieves observer information.
- Listen to 'eloquent.*' wildcard instead of class-based events
- Extract action name from event string (e.g., "eloquent.created: ...")
- Receive Model directly instead of ModelEvent wrapper
- Update shouldRecord() to filter by action name and ignored models
- Replace MODEL_EVENTS constant with MODEL_ACTIONS list
- Delete ModelListener.php (no longer needed with string events)
- Fix Dispatcher import in Capsule/Manager.php
- Update connection() return types to use ConnectionInterface
- Add explanatory comments to phpstan-ignore annotations
- Update EventWatcher to check for 'eloquent.' prefix instead of
  removed MODEL_EVENTS constant
- Update ModelWatcherTest to use 'actions' config with string action
  names instead of old 'events' config with class names
- Update namespaces from Illuminate to Hypervel
- Add strict_types declaration
- Add typed model properties
- Fix upsert mock return values
- Update namespaces from Illuminate to Hypervel
- Add strict_types declaration
- Add typed model properties
- Fix mock types (QueryBuilder instead of stdClass)
- Fix detach() assertions (returns int, not bool)
- Add mock connection setup for withDefault() tests
- Fix morphInstanceTo/getActualClassNameForMorph to accept string|int
  (supports integer morph map keys like Laravel)
…tionTest

- Update namespaces from Illuminate to Hypervel
- Add strict_types declaration
- Add typed model properties
- Fix mock types (Builder instead of stdClass)
- Fix getDates() return type
- Use Testbench\TestCase for container access
- Use Model mock instead of string for pivotParent
- Rename model stubs to avoid conflicts
…EloquentRelationshipsTest

- Update namespaces from Illuminate to Hypervel
- Add strict_types declaration
- Add typed model properties
- Fix method signature types for custom relationship factory methods
- Add return types to getConnection and getTable overrides
- Add isExpression mock expectation for Grammar
- Reorder class declarations for inheritance
- Update namespaces from Illuminate to Hypervel
- Add strict_types declaration
- Add typed model properties
- Add return types to Relation stub methods
- Add andReturn(1) to update mock expectations
- Add mock connection setup for testMacroable
- Port all Laravel HTTP Resource classes with proper Hypervel namespaces
- Create JsonResponse wrapper class for Laravel API compatibility
- Add $original property support for resource response customization
- Add encoding options parameter to Response::json() to match Laravel
- Fix inheritance hierarchy so AnonymousResourceCollection properly
  extends JsonResource through ResourceCollection

The JsonResponse wrapper provides Laravel's API (getData, setData, header,
etc.) while delegating to PSR-7 internally for Swoole compatibility.
- Update JsonResourceTest and ResourceCollectionTest to pass Request
  to toArray() (Laravel API requires Request parameter)
- Broaden Kernel::getResponseForException() return type from concrete
  Hyperf Response to ResponseInterface to support JsonResponse
- Non-wildcard listeners now receive only payload args, not event name
- Object events use the event object itself as payload (Laravel behavior)
- Wrap all payloads in arrays for consistent spreading
- Update queue handler methods to use correct payload index
- Remove obsolete unset() that was stripping event name from job data
- Delete obsolete ModelListenerTest (tested deleted ModelListener class)
- Update EventsDispatcherTest closures to match new callback signatures

This aligns Hypervel's event system with Laravel's API where listeners
receive `function(...$payload)` instead of `function($event, $payload)`.
- ListenerProvider now returns isWildcard flag with each listener
- EventDispatcher passes event name string (not object) to listeners
- Wildcard listeners receive (eventName, ...payload) with spread args
- Update EventWatcher to handle object events in payload[0]
- Update ListenerProviderTest for new return structure
…rceModelTest

- Update namespaces from Illuminate to Hypervel
- Add strict_types declaration
- Update exception messages and class_alias calls
- Update namespaces from Illuminate to Hypervel
- Add strict_types declaration
- Add typed model properties
- Add return type to anonymous class newModelQuery method
- Add bool type to anonymous class exists property
- Update namespaces from Illuminate to Hypervel
- Add strict_types declaration
- Add typed model properties
Add modern PHP type declarations to properties and methods through
approximately line 1527. Remaining methods from newModelInstance()
onwards still need updating.
Add modern PHP type declarations to all remaining properties and methods
from newModelInstance() through the end of the file.
- Make $model property nullable to allow access before setModel() is called
- Change increment/decrement $amount to mixed to support custom castable types
- Change get() return type to BaseCollection to support afterQuery transformations
- Change bare return statements to return null in __call/__callStatic
- Update DestroyStub to return Collection instead of array from get()
- Update FindWithWritePdoStub to return Model mock instead of string
- Update DatabaseSoftDeletingScopeTest getModel() to return Model mock
- Update DatabaseEloquentBuilderTest relation mocks to use Relation class
- Update DatabaseEloquentBuilderTest find/get mocks to return proper types
- Fix findOr() return type to mixed (callback can return any value)
- Replace spy() with full mock in SoftDeletesIntegrationTest
- Fix BelongsToMany tests to use QueryBuilder mock instead of stdClass
- Fix testValueOrFailMethodWithModelFound to use makePartial() and forceFill()
- Fix testFindOrMethodWithMany tests to expect empty Collection (not callback)
- Remove unused stdClass imports
- Change cursorPaginate() return type to use CursorPaginatorContract interface
  to match the underlying paginateUsingCursor() method's return type
- Remove redundant @return $this docblocks from QueriesRelationships trait
  since native static return types provide the same information and PHPStan
  treats $this vs static as conflicting types
- Add @var annotation in nested-set QueryBuilder to indicate get() returns
  Eloquent\Collection (which has getDictionary()), not BaseCollection
- Apply php-cs-fixer formatting to modified files
SQLite creates this file when DB_DATABASE=testing is set for MySQL/MariaDB/PostgreSQL
tests and a test uses the SQLite connection (which shares the same env var).
- Add Scout full-text search package (Meilisearch, Typesense, Collection, Database engines)
- Add Scout tests (unit, feature, and integration tests)
- Add VuePress documentation from upstream
- Move porting-from-laravel.md to docs/ai/
- Add Meilisearch and Typesense integration test base classes
- Update composer.json with Scout package and dependencies
- Add Scout env vars to .env.example
- Update .gitignore for docs build artifacts
- Move tests/Scout/Integration/ to tests/Integration/Scout/
- Update namespaces to Hypervel\Tests\Integration\Scout\*
- Add .github/workflows/scout.yml with Meilisearch and Typesense jobs
- Fix newQueuedJob() to pass mailable instance to SendQueuedMailable
- Add regression test to verify mailable is correctly passed to queued job
- Bump Application::VERSION to 0.4
… option

Update getOptions() in table generator commands to:
- Filter out parent's --path option to avoid conflict
- Add custom --path with -p shortcut and command-specific help text

Affected commands:
- BatchesTableCommand
- CacheTableCommand
- CacheLocksTableCommand
- NotificationTableCommand
- QueueTableCommand
- QueueFailedTableCommand
- SessionTableCommand
- TestCommand
- Update imports from Hyperf to Hypervel namespaces
- Use Hypervel\Contracts\Pagination\* instead of Hyperf\Contract\*
- Use Hypervel\Pagination\* instead of Hyperf\Paginator\*
- Use Hypervel\Database\Connection instead of Hyperf\Database\Connection
- Remove Hyperf\Tappable\tap import (tap() is globally available)
- Add phpstan-ignore for unresolvable generic type issues
- Use Hypervel\Database\Eloquent\Scope instead of Hyperf\Database\Model\Scope
- Use Collection as callback parameter type (matches BuildsQueries contract)
- Add @var annotation to assert EloquentCollection at runtime
- Add phpstan-ignore for searchable()/unsearchable() macros
Replace Hyperf's registerCallback() approach with Laravel-style observer
pattern for the Hypervel database package port.

- Create ModelObserver with coroutine-safe Context-based syncing state
- Update Searchable trait to use observer instead of registerCallback
- Keep isSearchSyncingEnabled() as Hypervel-specific addition for testing
- Fix test imports (pagination, dispatcher contracts, mock expectations)
- Update Meilisearch and Typesense integration test base classes to use
  shared migrations from tests/Scout/migrations/
- Apply php-cs-fixer formatting to Scout files
Add $migrateRefresh = true to both NestedSet test classes to ensure
their custom migrations always run, regardless of test ordering.

Without this, when Scout tests (or other RefreshDatabase tests) run
first and set RefreshDatabaseState::$migrated = true, NestedSet tests
would skip their migrations and fail with "no such table" errors.
Use @phpstan-return annotation for get() to specify the narrower
Collection return type while keeping BaseCollection as the native
type (required for pluck() compatibility). Remove redundant @return
docblock from types test file since `: static` already declares the type.
All changes from upstream/0.4 have been manually reviewed and
incorporated into feature/hyperf-decouple with necessary adaptations
for Hypervel's architecture (coroutine-safe patterns, Context usage,
namespace changes, etc.). Using "ours" strategy to mark the merge
point without overwriting our adapted implementations.
- Remove @group integration annotations from all Scout integration tests
  (we no longer use PHPUnit groups for test filtering)
- Update base test case docblocks to remove outdated @group notes
- Rename SCOUT_AFTER_COMMIT to SCOUT_QUEUE_AFTER_COMMIT for clarity
- Add missing Typesense client-settings to match Laravel Scout:
  - path in nodes
  - nearest_node configuration
  - connection_timeout_seconds, healthcheck_interval_seconds,
    num_retries, retry_interval_seconds with env vars
  - import_action with env var
Port of laravel/scout#958. Properly escape double quotes and
backslashes in filter string values to prevent empty or wrong
result sets when filtering for values containing these characters.
Following Laravel's InteractsWithRedis pattern:
- Auto-skip when using default host/port and no explicit env var set
- Fail (not skip) when explicit config exists but connection fails
- This catches misconfigurations while allowing tests to skip gracefully
  when services aren't available

Also removes obsolete RUN_*_INTEGRATION_TESTS env vars - the auto-skip
logic handles this automatically now.
// resolution to ConnectionResolverInterface.
$this->container->instance(
ConnectionResolverInterface::class,
new SimpleConnectionResolver($this->manager)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be a pooled ConnectionResolver here? Or it must be SimpleConnectionResolver for Capsule use here?

Copy link
Contributor Author

@binaryfire binaryfire Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @albertcht,

I think it needs to be SimpleConnectionResolver (non-pooled) for Capsule.

Capsule is designed for standalone, simple database usage (eg. unit tests and basic scripts) without bootstrapping a full application. These are typically non-production, non-concurrency scenarios where connection pooling adds complexity without benefit.

For Hypervel specifically:

  1. Test portability - 40 of our Laravel-ported tests use Capsule for isolated database testing (e.g., DatabaseEloquentBelongsToManyChunkByIdTest, DatabaseEloquentLocalScopesTest, DatabaseEloquentIntegrationTest). These tests verify Eloquent behavior like relationship queries, scopes, and chunking - none of which changes based on how the connection was obtained. Keeping Capsule unpooled means we can port tests like these directly.

  2. Pooling is tested separately - We have dedicated integration tests for connection pooling behavior: PooledConnectionStateTest, InMemorySqliteSharedPdoTest, PoolConnectionManagementTest, ConnectionCoroutineSafetyTest. These test the pool-specific concerns (context isolation, checkout/release, shared PDO) using the full app infrastructure.

  3. Infrastructure requirements - The pooled ConnectionResolver requires PoolFactoryDbPoolConfigInterface. Capsule creates a minimal container without any of this infrastructure, so pooling would fail at $container->get(ConfigInterface::class).

  4. Simple use cases - For unit tests and basic scripts that just want quick Eloquent access, the full pooling infrastructure is unnecessary overhead.

The pooled ConnectionResolver is for production Swoole apps and Artisan commands with high concurrency. SimpleConnectionResolver is for standalone/simple contexts where Capsule is typically used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a docblock to the Capsule Manager class explaining this

Documents why Capsule uses SimpleConnectionResolver instead of the pooled
ConnectionResolver, including test portability and infrastructure requirements.
Required by meilisearch/meilisearch-php and typesense/typesense-php for
auto-discovering PSR-17/PSR-18 HTTP client implementations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants