-
-
Notifications
You must be signed in to change notification settings - Fork 14
feat: framework refactor + decouple from Hyperf #349
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
binaryfire
wants to merge
512
commits into
hypervel:0.4
Choose a base branch
from
binaryfire:feature/hyperf-decouple
base: 0.4
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Performance optimization for Swoole workers: - Add plural(), singular(), pluralStudly() methods to StrCache - Cache results for finite inputs (class names, relationship names) - Update database package to use StrCache for table name inference, relationship guessing, and pivot table naming Files using StrCache for pluralization: - Model::getTable() - table name from class name - Model::childRouteBindingRelationshipName() - route binding relationships - BelongsToMany::guessInverseRelation() - inverse relation name - QueriesRelationships::whereAttachedTo() - relationship name guessing - HasRelationships::morphToMany() - pivot table name - AsPivot::getTable() - pivot table name from class name - Factory - relationship guessing for has/for methods
DatabaseManager::connection() was passing null to ConnectionResolver when no name was specified, causing ConnectionResolver to use its own default instead of checking the Context override set by usingConnection(). Now DatabaseManager resolves the default name (checking Context first) before passing to ConnectionResolver, ensuring usingConnection() properly affects all queries within its callback.
SchemaProxy was using ConnectionResolver directly, bypassing DatabaseManager and therefore not respecting the Context override set by usingConnection(). Now routes through DatabaseManager::connection() which properly checks Context for per-coroutine connection overrides.
- testUsingConnectionAffectsDbConnection: Verifies DB::connection() without explicit name returns connection matching usingConnection override. Would have failed before DatabaseManager::connection() fix. - testUsingConnectionAffectsSchemaConnection: Verifies Schema::connection() without explicit name uses usingConnection override. Would have failed before SchemaProxy fix.
ConnectionResolver::getDefaultConnection() now checks Context for the per-coroutine default connection override set by usingConnection(). This ensures code that directly injects ConnectionResolverInterface gets the same override behavior as code using DatabaseManager or the DB/Schema facades. The DEFAULT_CONNECTION_CONTEXT_KEY constant is now defined in ConnectionResolver as the single source of truth.
These tests demonstrate that per-request state on a Connection leaks to subsequent coroutines that reuse the same pooled connection: - Query logging state ($loggingQueries, $queryLog) - Query duration handlers ($queryDurationHandlers) - Total query duration ($totalQueryDuration) - beforeStartingTransaction callbacks - readOnWriteConnection flag - pretending flag All 6 tests fail, confirming the bugs exist. The fix will add a resetForPool() method to Connection and call it from PooledConnection on release.
When a connection is released back to the pool, resetForPool() now clears all per-request state: - beforeExecutingCallbacks and beforeStartingTransaction callbacks - Query logging state (loggingQueries, queryLog) - Query duration tracking (totalQueryDuration, queryDurationHandlers) - readOnWriteConnection flag - pretending flag - recordsModified state This ensures each coroutine/request gets a clean connection without leaked state from previous requests. PooledConnection::release() now calls resetForPool() instead of individual cleanup methods.
Convert test files to use Hypervel database classes instead of Hyperf: - Hyperf\Database\Model\* → Hypervel\Database\Eloquent\* - Hyperf\Database\Query\* → Hypervel\Database\Query\* - Hyperf\Database\Schema\* → Hypervel\Database\Schema\* - Hyperf\Database\Events\* → Hypervel\Database\Events\* - Hyperf\Database\ConnectionInterface → Hypervel\Database\ConnectionInterface - Hyperf\Database\Exception\QueryException → Hypervel\Database\QueryException Also converts workbench User model and UserFactory to use Hypervel's class-based factory system (HasFactory trait + Factory class) instead of Hyperf's legacy $factory->define() pattern. Fixes method signature compatibility issues (e.g., getRouteKeyName). Work in progress - more test files remain to be converted.
…base Additional test files converted: - NestedSet tests and migrations - Notifications tests - Permission migrations - Router stubs - Sanctum migrations - Sentry feature tests - Telescope tests - Validation tests and migrations - Queue migrations (partial) Remaining: 4 Queue test files
…atabase Final batch: - Queue test files (QueueDatabaseQueueUnitTest, DatabaseUuidFailedJobProviderTest, DatabaseFailedJobProviderTest, QueueDatabaseQueueIntegrationTest) All 83 Hyperf\Database imports across 45 test files have been converted to use Hypervel\Database equivalents.
- ValidationUniqueRuleTest: Fix UnitEnum import and property type - ValidationValidatorTest: Fix UnitEnum import and property type - PriceFactory: Add :array return type to definition() - HasNode: Fix replicate() return type from Model to static - Permission migration: Fix getConnection() return type to ?string
- Rewrite updating/deleting event handlers to use registerCallback() in boot() - Remove unused event class imports
- Use static::saving(), static::creating(), etc. instead of registerCallback() - Hypervel's Model uses Laravel-style event registration, not Hyperf's registerCallback() - Fixed: HasNode trait, PersonalAccessToken, pivot event tests
- Fix getRelationExistenceQuery() return type from mixed to EloquentBuilder - Fix parameter naming and types to match parent Relation class
- Replace Hyperf\Collection\Collection with Hypervel\Support\Collection in tests - Fix QueryException constructor calls to match new signature (connectionName, sql, bindings, previous)
- Builder::find() and Builder::first() return stdClass, not array - Convert array returns to (object) casts in test mocks
Create new contracts package following Laravel's directory structure. For now, only includes collection-related contracts: - Hypervel\Contracts\Support\Arrayable - Hypervel\Contracts\Support\Jsonable - Hypervel\Contracts\Support\CanBeEscapedWhenCastToString All contracts modernized with PHP 8.4 types (strict_types, return types).
Create new collections package with: - composer.json with dependencies on hyperf/conditionable, hyperf/macroable, hypervel/contracts - LICENSE.md Package uses Hypervel\Support namespace to match Laravel's pattern where illuminate/collections uses Illuminate\Support namespace.
Move previously modernized Arr.php to the new collections package. Updated imports to use Hypervel\Contracts\Support\Arrayable and Jsonable. Hyperf imports will be updated once the remaining collection classes are copied over.
Copy Collection.php from illuminate/collections and update: - Namespace: Illuminate\Support -> Hypervel\Support - Contract imports: Illuminate\Contracts -> Hypervel\Contracts - Uses Hyperf\Macroable\Macroable Type modernization will be done in a separate pass.
Copy LazyCollection.php from illuminate/collections and update: - Namespace: Illuminate\Support -> Hypervel\Support - Contract imports: Illuminate\Contracts -> Hypervel\Contracts - Uses Hyperf\Macroable\Macroable Type modernization will be done in a separate pass.
Copy Enumerable.php from illuminate/collections and update: - Namespace: Illuminate\Support -> Hypervel\Support - Contract imports: Illuminate\Contracts -> Hypervel\Contracts Type modernization will be done in a separate pass.
- HigherOrderCollectionProxy.php: proxy for higher-order collection methods - ItemNotFoundException.php: exception for sole() when no items found - MultipleItemsFoundException.php: exception for sole() when multiple items found All updated with Hypervel\Support namespace and PHP 8.4 types.
- EnumeratesValues.php: core trait for collection methods, copied from Laravel - TransformsToResourceCollection.php: moved from support package EnumeratesValues updated with Hypervel namespaces and uses Hyperf\Conditionable\Conditionable.
Previous commit moved the old Hypervel version. This replaces it with the fresh Laravel version with updated namespaces.
- functions.php: enum_value() helper function - helpers.php: collect(), data_fill(), data_get(), data_set(), data_forget(), data_has(), head(), last(), value(), when() All updated with Hypervel namespaces.
Main composer.json: - Add Hypervel\Contracts namespace mapping - Update Hypervel\Support to array with collections and support paths - Add collections function files to autoload - Add hypervel/collections and hypervel/contracts to replace Support package: - Remove hyperf/collection dependency, add hypervel/collections - Delete old Collection.php and LazyCollection.php (now in collections) - Delete duplicate contracts (Arrayable, Jsonable, CanBeEscapedWhenCastToString) now in hypervel/contracts
Replace Hyperf\Collection imports with Hypervel\Support: - ItemNotFoundException - MultipleItemsFoundException - Enumerable Keep Hyperf\Macroable\Macroable as we're still using Hyperf's macroable trait.
- Add native type hints to properties and method signatures - Add return types where applicable - Keep docblocks with descriptions and generic type info - Replace bare return statements with explicit null returns - Import Arrayable and Stringable classes - Shorten namespace references in docblocks Methods modernized: range through join
- Continue adding native type hints to method signatures - Methods: keys through select - Import Closure class - Update Enumerable references in docblocks
__call() checks hasNamedScope() for every unknown method, but model can be null before setModel() is called. This caused crashes when any method was called on a Builder before its model was set. Match Laravel's defensive null check: $this->model && $this->model->...
- Update namespaces from Illuminate to Hypervel
- Use Testbench\TestCase for container support
- Add typed properties to model stubs
- Fix mock return types for strict typing:
- newCollection() returns Collection, not string
- newFromBuilder() uses partial mocks with andReturnUsing to satisfy
static return type while preserving getAttribute behavior
- from() uses andReturnSelf() for fluent interface
- insert/insertOrIgnore/insertGetId/etc use correct return types
- raw() returns Expression, not string
- Tests verify same behavior with Hypervel's stricter type system
- Update namespaces from Illuminate to Hypervel - Add declare(strict_types=1)
…uple # Conflicts: # src/database/src/DatabaseManager.php
The merge from feature/database-improvements incorrectly removed the classmap entry for tests/Validation/Enums.php. This restores it.
- Update namespaces from Illuminate to Hypervel - Add declare(strict_types=1) - Add typed properties to model stubs - Remove phpunit_version_compare dependency (use assertContainsOnly directly)
- Update namespaces from Illuminate to Hypervel - Add declare(strict_types=1) - Use Testbench\TestCase for container support - Add return types to overridden methods - Fix DynamicRelationModel2::newQuery() to setModel() on the Builder (Hypervel's strict typing on Relation::$related requires non-null model)
Container changes: - Add singleton() method as alias for bind() until Laravel's container is ported. In Hyperf/Swoole, all bindings are singletons by default. Test changes: - Update DatabaseEloquentFactoryTest namespaces and typed properties - Skip test with TODO(laravel-container-port) marker - requires Container::setInstance(null) and other Laravel-specific behaviors - Fix Price model typed property
- Update namespaces from Illuminate to Hypervel - Add declare(strict_types=1) - Add typed properties to model stubs - Add return types to boot() and apply() methods
- DatabaseEloquentHasManyCreateOrFirstTest: 8 tests - DatabaseEloquentHasManyTest: 25 tests - DatabaseEloquentHasManyThroughCreateOrFirstTest: 8 tests Uses concrete test stub (EloquentHasManyRelatedStub) for makeMany test to properly verify distinct instances are created - stronger than original Laravel test which used loose mock expectations. Updated porting guide with concrete stub pattern for when andReturnSelf() isn't sufficient.
…tests Bug fix: - JoinClause::__construct() now accepts ExpressionContract|string for $table parameter, matching the property type declaration and Laravel's behavior Ported tests: - DatabaseEloquentHasManyThroughIntegrationTest: 27 tests - DatabaseEloquentHasOneOfManyTest: 34 tests
- DatabaseEloquentHasOneTest: 19 tests - DatabaseEloquentHasOneThroughIntegrationTest: 17 tests Uses partial mocks for related models to allow real Model methods (setAttribute, forceFill) to work while still setting mock expectations.
35 tests with factories for testing hasOneThrough ofMany relations
Port 133 tests from Laravel's integration test suite. Bug fixes discovered during porting: - Cursor::parameter(): Return type changed from ?string to string|int|null to support integer cursor parameters (e.g., auto-increment IDs) - Builder::forPageAfterId(): $lastId type changed from ?int to string|int|null to support non-incrementing string keys (UUIDs, etc.) - Str::uuid7(): Return type changed to UuidInterface|string to support custom UUID factories that return strings
- DatabaseEloquentIntegrationWithTablePrefixTest (3 tests) - DatabaseEloquentInverseRelationHasManyTest (11 tests) - DatabaseEloquentInverseRelationHasOneTest (9 tests) - DatabaseEloquentInverseRelationMorphManyTest (15 tests) - DatabaseEloquentInverseRelationMorphOneTest (11 tests) - DatabaseEloquentInverseRelationTest (21 tests) Updates include namespace changes, typed properties, return types, and local model stubs for self-contained tests.
- 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.Stringablewas 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
RunTestsInCoroutineto 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:
hypervel/contractspackage, the Hyperf database code is split across 3 packages, the Hyperf pagination package ishyperf/paginatorand nothyperf/pagination)static::registerCallback('creating')vsstatic::creating())ConfigProviderand LaravelServiceProviderpatterns across different packages is confusing for anyone who doesn't know HyperfThis 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:
hypervel/contractsfor contracts) and then have to spend a lot of time grepping for things to find them.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
illuminate/databaseportilluminate/collectionsportilluminate/paginationportilluminate/contracts)hyperf/pool)Macroableto a separate package for Laravel parityRemoved Hyperf dependencies so far
Database package
The big task was porting the database package, making it coroutine safe, implementing performance improvements like static caching and modernising the types.
whereLike,whereNot,groupLimit,rawValue,soleValue, JSON operations, etc.Collections package
Contracts package
Support package
hyperf/tappable,hyperf/stringable,hyperf/macroable,hyperf/codecdependenciesStr,Envand helper classes from LaravelHypervel\Contextwrappers (will be portinghyperf/contextsoon)Number::useCurrency()wasn't actually setting the currency)Coroutine safety
withoutEvents(),withoutBroadcasting(),withoutTouching()now use Context instead of static propertiesUnsetContextInTaskWorkerListenerto clear database context in task workersConnection::resetForPool()to prevent state leaks between coroutinesDatabaseTransactionsManagercoroutine-safeBenefits
Testing status so far
What's left (WIP)
The refactor process
Hyperf's Swoole packages like
pool,coroutine,contextandhttp-serverhaven'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
ConfigProviderand 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.