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.

- Fix helpers.php: add missing return null in optional(), use while loop in retry()
- Fix CacheCommandMutex: forceRelease() returns void, return true after
- Fix ProcessInspector: remove incorrect @var annotation
- Fix Onceable: add missing return null in tryFromTrace()
- Fix ModelWatcher: use ModelEvent instead of non-existent Event class
- Fix Response/CoreMiddleware/Logger: use toJson()/toArray() for Hypervel contracts
- Update ResponseTest to use Hypervel contracts with proper toJson() implementation
- Worker: Fix broken tap pattern, use until() like Laravel, update EventDispatcher type
- FailedJobProviders: Replace dynamic whereConnection/whereQueue with explicit where()
- HasFactory: Add phpstan ignore for optional $factory property check
- AutoScaler: Add type annotation to fix sum() type inference
- Horizon RedisQueue: Add type assertion for RedisJob in pop() callback
- Telescope migration: Fix getConnection return type to ?string
- Tests: Update event namespaces and Connection mock for database port
Worker now requires Hypervel\Event\Contracts\Dispatcher (for until() method)
instead of Psr\EventDispatcher\EventDispatcherInterface.
The test suite was hanging at ~25% due to exponential growth of
$modifiedItems in ReloadDotenvAndConfig.

Root cause:
- Repository::$afterSettingCallback was static, so test-created
  Repository instances polluted the shared $modifiedItems array
- array_merge() appends integer keys instead of overwriting them,
  causing exponential growth when tests used numeric config keys

Fix:
- Make $afterSettingCallback instance-scoped so only the container's
  Repository triggers the callback
- Use array_replace() instead of array_merge() to handle integer keys
  correctly
- Update Contains and DoesntContain rules to use Hypervel\Contracts\Support\Arrayable
  instead of Hyperf\Contract\Arrayable
- Add null guard in ValidatesAttributes::parseTable() before Str::startsWith() call
  since getConnectionName() can return null and the newly ported Str has strict types
Move Hypervel\Broadcasting\Contracts\* to Hypervel\Contracts\Broadcasting\*
for Laravel directory structure parity.
Move Hypervel\Queue\Contracts\* to Hypervel\Contracts\Queue\*
for Laravel directory structure parity.

Update phpstan ignore pattern for new namespace.
Relocate all Auth contracts from src/auth/src/Contracts/ to
src/contracts/src/Auth/ to match Laravel's directory structure.

- Authenticatable, Factory, Guard, StatefulGuard, UserProvider
  moved to Hypervel\Contracts\Auth
- Authorizable, Gate moved to Hypervel\Contracts\Auth\Access
- Updated all 68 files referencing the old namespaces
Relocate all Bus contracts from src/bus/src/Contracts/ to
src/contracts/src/Bus/ to match Laravel's directory structure.

- Dispatcher, QueueingDispatcher (Laravel matches)
- BatchRepository, PrunableBatchRepository (Hypervel-specific)
- Updated all 42 files referencing the old namespaces
Relocate all Cache contracts from src/cache/src/Contracts/ to
src/contracts/src/Cache/ to match Laravel's illuminate/contracts
directory structure.

Updated namespace from Hypervel\Cache\Contracts to
Hypervel\Contracts\Cache across all src and test files.
Relocate Cookie contract from src/cookie/src/Contracts/ to
src/contracts/src/Cookie/ to match Laravel's illuminate/contracts
directory structure.

Updated namespace from Hypervel\Cookie\Contracts to
Hypervel\Contracts\Cookie across all src and test files.
Move contract-documented exceptions that may be thrown/caught across packages:
- Encryption: DecryptException, EncryptException
- Filesystem: LockTimeoutException
- Cache: LockTimeoutException
Cross-cutting exception used by router package's ThrottleRequests middleware.
Relocate contracts from Hypervel\Router\Contracts to
Hypervel\Contracts\Router for centralized contract management.
Relocate contracts from Hypervel\Session\Contracts to
Hypervel\Contracts\Session for centralized contract management.
Relocate contracts from Hypervel\Translation\Contracts to
Hypervel\Contracts\Translation for centralized contract management.
Relocate contracts from Hypervel\Validation\Contracts to
Hypervel\Contracts\Validation for centralized contract management.
Relocate contracts from Hypervel\Hashing\Contracts to
Hypervel\Contracts\Hashing for centralized contract management.
- Application -> Contracts\Foundation\Application
- ExceptionRenderer -> Contracts\Foundation\ExceptionRenderer
- ExceptionHandler -> Contracts\Debug\ExceptionHandler
- ShouldntReport -> Contracts\Debug\ShouldntReport
- Add DEPTH_KEY constant for DI depth tracking
- Add UnitEnum|string support for all key methods using enum_value()
- Add setMany() for setting multiple key-value pairs
- Add copyFromNonCoroutine() for copying from non-coroutine context
- Add destroyAll() for destroying all context data except DEPTH_KEY
- Skip __call method (dead code - never used, all calls are static)
Convenience wrapper to access parent coroutine's context.
Uses Hyperf\Coroutine\Coroutine for inCoroutine/parentId methods.
- Delete tests/Core/ContextTest.php and ContextEnumTest.php (moved to tests/Context/)
- Delete src/core/src/Context/ directory (replaced by src/context/)
The value() function is defined in the global namespace in
collections/helpers.php, not in Hypervel\Support namespace.
The previous test expected a TypeError for int-backed enums, but that
was a side-effect of the Hyperf parent class signature (string $id).
Now that Context is standalone, int-backed enums work correctly as
array keys. Updated test to verify the correct behavior.
ApplicationContext::getContainer() returns Hypervel\Contracts\Container\Container
which guarantees call() exists, making the method_exists() check always true.
This allows Hypervel code using Hypervel\Context\Context to share
the static $nonCoContext storage with Hyperf vendor code that still
uses Hyperf\Context\Context directly.

TODO: Remove extends and uncomment $nonCoContext once all Hyperf
dependencies are removed.
Same pattern as Context class - extend parent to share static $container
with Hyperf vendor code until all Hyperf dependencies are removed.
- ApplicationContext now extends Hyperf\Context\ApplicationContext
  to share static container storage
- Update tests that mock ContainerInterface to use
  Hypervel\Contracts\Container\Container instead
- Update tests using Hyperf\Di\Container to use
  Hypervel\Container\Container instead
- MailMailableTest: Use Testbench, bind mocks via $this->app->set()
- MailMailerTest: Use Testbench, simplify mockContainer
- MailManagerTest: Use Testbench with setUp to mock ViewFactory
- MailFailoverTransportTest: Partial update (in progress)
Update tests that were using Hyperf\Di\Container or mocking
Hyperf\Contract\ContainerInterface to use Hypervel\Container\Container
and Hypervel\Contracts\Container\Container instead.

This is required because ApplicationContext::getContainer() now returns
Hypervel\Contracts\Container\Container, not Psr\Container\ContainerInterface.

- Mail tests: Convert to Testbench TestCase for proper container bootstrap
- Router tests: Use Hypervel container and contract
- Notifications tests: Use Hypervel container
Update tests that were using Hyperf\Di\Container or mocking
Psr\Container\ContainerInterface to use Hypervel\Container\Container
and Hypervel\Contracts\Container\Container instead.
- Update Queue tests to use Hypervel container
- Delete duplicate Context tests from tests/Core (moved to tests/Context)
- Fix phpstan error in Context.php (commented-out @var tag)
- Run php-cs-fixer on entire codebase
- Delete old Context files from src/core/src/Context (now in src/context)
- Move context helpers.php from core to context package
- Update composer.json files for helpers.php location
- Fix types assertion for looser Collection return type
- Port EloquentModelCustomEventsTest
- Port EloquentModelDateCastingTest
- Port EloquentModelDecimalCastingTest
- Port EloquentModelEncryptedCastingTest
- Port EloquentModelEncryptedDirtyTest
- Add cipher, key, and previous_keys to workbench app.php config
…to Argon hashers

Tests:
- Port EloquentModelEnumCastingTest
- Port EloquentModelHashedCastingTest
- Port Enums.php fixture

Source fixes:
- Add verifyConfiguration(), isUsingCorrectAlgorithm(), isUsingValidOptions() to ArgonHasher
- Add isUsingCorrectAlgorithm() override to Argon2IdHasher for 'argon2id' algorithm check
- These methods were missing, causing hashed cast verification to always pass for Argon hashes
Tests ported:
- EloquentModelImmutableDateCastingTest
- EloquentModelJsonCastingTest
- EloquentModelLoadCountTest
- EloquentModelLoadMaxTest
- EloquentModelLoadMinTest
- EloquentModelLoadMissingTest
- EloquentModelLoadSumTest
- EloquentModelRefreshTest

Source fixes:
- Change newPivot() return type from Pivot to Model/self in:
  - Model.php
  - InteractsWithPivotTable.php (also newExistingPivot)
  - MorphToMany.php
- This allows AsPivot trait to be used on models that don't extend Pivot
The whereNotNull() method passes null to where() when called without
arguments (to filter items that are not null values). This failed
because the $key type hint was too strict (callable|string).

data_get($item, null) correctly returns the item itself, making null
a valid key. Updated where() and operatorForWhere() in EnumeratesValues
trait and the Enumerable interface to accept callable|string|null.

Also ports EloquentModelRelationAutoloadTest.
Converted 11 Laravel integration tests to Hypervel:
- EloquentModelScopeTest
- EloquentModelStringCastingTest
- EloquentModelTest
- EloquentModelWithoutEventsTest
- EloquentMorphConstrainTest
- EloquentMorphCountEagerLoadingTest
- EloquentMorphCountLazyEagerLoadingTest
- EloquentMorphEagerLoadingTest
- EloquentMorphLazyEagerLoadingTest
- EloquentMorphManyTest
- EloquentMorphOneIsTest

Standard conversions: namespace changes, strict types, property type
hints, and method return types.
Ported 6 Laravel integration tests:
- EloquentMorphToGlobalScopesTest
- EloquentMorphToIsTest
- EloquentMorphToLazyEagerLoadingTest
- EloquentMorphToSelectTest
- EloquentMorphToTouchesTest
- EloquentMultiDimensionalArrayEagerLoadingTest

Removed `: static` return type from flatMap() in Enumerable interface
and EnumeratesValues trait. Eloquent\Collection::collapse() returns
base collection, which violates the `: static` contract. Laravel has
no return type on flatMap(), so this aligns with upstream.
Ported 4 Laravel integration tests:
- EloquentNamedScopeAttributeTest
- EloquentPaginateTest
- EloquentPivotEventsTest
- EloquentPivotSerializationTest

Fixed getQueryForModelRestoration() type hint in queue package to allow
string IDs. Pivot models use composite string keys which were rejected
by the strict array|int type. Changed to array|int|string to match
Laravel's untyped signature.
Standard conversion: namespace changes, strict types, property type
hints, and method return types.
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