-
-
Notifications
You must be signed in to change notification settings - Fork 15
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
base: 0.4
Are you sure you want to change the base?
Conversation
|
@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: 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. |
- 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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
-
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. -
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. -
Infrastructure requirements - The pooled
ConnectionResolverrequiresPoolFactory→DbPool→ConfigInterface. Capsule creates a minimal container without any of this infrastructure, so pooling would fail at$container->get(ConfigInterface::class). -
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.
There was a problem hiding this comment.
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.
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.