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.

- Add Htmlable import to Paginator and CursorPaginator contracts
- Fix render() return type in CursorPaginator contract (string → Htmlable)
- Add missing return null in LengthAwarePaginator::nextPageUrl()
- Remove redundant filter_var check in isValidPageNumber (already typed as int)
- Fix getParametersForItem() types with @var annotation after flip()
- Fix linkCollection() return type with @var annotation
- Remove setCollection() @phpstan-ignore (covered by global ignore)
- Add global generics.variance ignore for Laravel's covariant template pattern
- Add getSchemaState() stub to base Connection (throws RuntimeException)
- Add compileEnableForeignKeyConstraints/compileDisableForeignKeyConstraints
  stubs to base Grammar
- Add dropVectorIndex() method to Blueprint (was missing)
- Add LogicException after transaction loop for impossible code path
- Add $sql parameter to MySQL/Postgres Processor subclasses
- Add inline PHPStan ignores for Eloquent hydration in BuildsQueries
- Add #[Override] attributes to concrete implementations
- Widen prepareBindingForJsonContains return type to mixed (SQLite needs it)
- Add ForeignIdColumnDefinition method ignore to phpstan.neon
- Add inline ignores for BlueprintState Fluent assignments
- Add inline ignores for Query\Builder driver-specific methods
- Add inline ignore for PostgresGrammar filter_var check
- Add inline ignores for SoftDeletingScope macro $this rebinding
- Remove overly restrictive PHPDoc from Migrator::write()
- Widen DatabaseMigrationRepository::getConnection() to ConnectionInterface
- Add inline ignore for Migrator::resolveConnection() interface return
Accepts both keyed arrays and lists, adds inline ignore for pass-through return
getMorphType, getForeignKeyName, getExistenceCompareKey exist on subclasses
(MorphTo, HasOneOrMany, etc.) but are called through base Relation type
- Fix malformed @param PHPDoc in loadMissingRelationshipChain
- Add ignore for withAggregate method chain type loss
- is_null() checks on template types (collection may contain nulls)
- instanceof checks after map callbacks (may transform to non-Model)
- getDictionary type loss through query chain
- new static loses template types for diff/intersect methods
- method.childReturnType/childParameterType for Relation covariance
- call_user_func void results (intentional pattern)
- booleanAnd.rightAlwaysTrue type narrowing
- DatabaseManager::reconnect() ConnectionInterface return
- AsCollection/AsEncryptedCollection implode and class-string types
- Fix registerMixin PHPDoc: string → object (real bug fix)
- Fix void return pattern in __callStatic (real code fix)
- Fix setModel return type ignore (legitimate PHPDoc/generics limitation)
- Add Collection method return type ignores (new static template loss)
- Add fresh/partition/getQueueableRelations ignores (method chain type loss)
- Model::toJson(): Add proper int param and string return types
- Model::toPrettyJson(): Add string return type
- MorphToMany::baseAttachRecord(): Change int to mixed for contravariance
- HasAttributes: Cast decimal precision to int (was string from explode)
- HasAttributes: Fix InvalidCastException to use $this not $this->getModel()
- HasAttributes: Add inline ignores for defensive catch and HigherOrderProxy
- HasEvents: Ignore is_subclass_of and staticMethod.nonObject for parent resolution
- HasTimestamps: Ignore arrayValues.list (array_values needed after unset gaps)
- HasVersion4Uuids: Ignore trait.unused (user-facing trait)
- Model::delete(): Ignore is_null defensive check, explicit return null
- Model::fresh(): Explicit return null
- Model::newEloquentBuilder(): Ignore is_subclass_of defensive check
- SupportsInverseRelations: Ignore instanceof.alwaysTrue (defensive)
- QueriesRelationships: Ignore nullCoalesce.offset (defensive fallback)
Add inline ignores for Eloquent\Builder methods called on return values
that PHPStan incorrectly types as Query\Builder (due to @mixin limitation):
- QueriesRelationships: callScope, mergeConstraintsFrom, withCasts
- BelongsToMany: getModels after addSelect chain
- HasEvents: Ignore return.type for flatten() losing class-string inference
- HasOneOrManyThrough: Ignore laravel_through_key dynamic property
- Blueprint: Ignore name dynamic Fluent attribute
- Add native type hints to all properties that can have them
- Add native parameter types to all method signatures
- Add native return types to all methods
- Import necessary classes (Connection, Dispatcher, QueryBuilder, Relations, UnitEnum)
- Keep PHPDoc @var annotations only for callable types with specific signatures
- Keep generic type annotations in PHPDoc for PHPStan
- Replace Hyperf\Contract\CanBeEscapedWhenCastToString with Hypervel version
- Add inline ignores for:
  - arrayValues.list (array_diff creates gaps)
  - return.type for mixin/template covariance issues
  - resolver interface vs concrete type
…able

- Builder: Ignore pluck() type loss for where clause 'boolean' field
- InteractsWithPivotTable: Ignore array_diff type for plucked IDs
- HasAttributes: Move catch ignore before catch line, move method.nonObject before return
- HasRelationships: Add conditionalType.alwaysFalse ignore for through()
- HasCollection: Add assign.propertyType ignore for static property
- QueriesRelationships: Fix method.notFound ignores to cover each line separately
- Builder: Separate ignores for pluck type loss in where clause handling
- Collection: Move return.phpDocType ignore to PHPDoc block
- QueriesRelationships: Add ignores for callback template type forwarding
- HasOneOrManyThrough: Add ignores for self-relation query methods and performJoin
- MorphTo: Add ignore for eager loading with declaring model
…t.type

- HasOneOrManyThrough: Add ignore for performJoin in addConstraints
- Relation: Move method.notFound ignore to the actual line with the call
Add native type hints to all properties, parameters, and return types.
Remove redundant PHPDoc annotations, keeping only generic type info.
- PendingHasThroughRelationship: Add strict_types, type properties and methods
- Prunable/MassPrunable: Add strict_types, return types
- SoftDeletes: Add union types for forceDestroy and callback methods
- BroadcastsEvents: Add proper union types (Channel|HasBroadcastChannel|array|null)
- HasCollection: Add return type to resolveCollectionFromAttribute
…tions

Real fixes:
- Model::delete() return type widened to int|bool|null for Pivot compatibility
- ModelInspector::inspect() database field type corrected to string|null

PHPStan ignores for genuine limitations:
- Collection template covariance (specific array shapes vs generic arrays)
- Generic type loss through closures, new static, dynamic instantiation
- @mixin forwarding returning Query\Builder instead of $this
- Template type covariance ($this vs static in generic contexts)
Real fixes:
- BelongsToMany::buildDictionary() PHPDoc corrected to return list<T> not array<string, T>

PHPStan neon patterns for repeating limitations:
- Relation @mixin forwarding: $this returns Builder instead of Relation
- BelongsToMany pivot intersection: object{pivot: ...}&TRelatedModel can't be tracked

Inline ignores for specific limitations:
- ModelInspector: Collection key type changes (values(), flip()) not tracked
- HasManyThrough::one(): template types lost through closure/tap
- HasOneThrough: Builder param lacks template type in inherited interface

Database package now passes PHPStan level 5 with zero errors.
- Copy Arr.php from Laravel and update namespace to Hypervel
- Add Arr::from() and other missing methods (8 total)
- Create Pluralizer.php for singularization/pluralization
- Add voku/portable-ascii and doctrine/inflector dependencies
- Begin type modernization of Arr.php (in progress)
All 59 methods now have native PHP 8.4 type hints.
Added imports for ItemNotFoundException and MultipleItemsFoundException.
- Use Hyperf\Collection\Enumerable instead of non-existent Hypervel class
- Fix float comparison in flatten() - compare against 1.0 not 1
- Add phpstan-ignore for is_object() false positive in match expression
- Fix Number::useCurrency() bug (was using Context::get instead of set)
- Fix Number::withLocale/withCurrency() to restore Context values correctly
- Replace Hyperf\Context imports with Hypervel\Context wrappers in support
- Remove hyperf/tappable dependency (use global tap() from helpers.php)
- Remove Hyperf\Support\value and env imports (use global helpers)
- Add comprehensive Number tests with coroutine isolation coverage
These commands were defining a --path option that already exists in the
parent GeneratorCommand class, causing Symfony Console to throw an error
about duplicate options. Remove the redundant definitions and delegate
to parent::getOptions() instead.

Also adds missing composer.json for the pool package.
- Run php-cs-fixer across codebase
- Fix Eloquent Collection merge() return type annotation
- Fix PHPStan ignore placement in Relation::getRelationExistenceQuery()
- Suppress expected error logs in PoolConnectionManagementTest
@binaryfire binaryfire marked this pull request as draft January 26, 2026 03:51
- Fix regex escape in Str::excerpt()
- Add blank line between trait uses in Collection and LazyCollection
- Remove redundant @param from ManagesFrequencies::yearlyOn()
- Copied Laravel's Database type files for better coverage
- Updated namespaces from Illuminate to Hypervel
- Added Factory.php and Casts type tests
- Updated Autoload.php with HasFactory, MassPrunable, HasDatabaseNotifications
- Fixed type assertions to match actual Hypervel return types:
  - saveMany/saveManyQuietly return iterable (not specific collection types)
  - notifications() returns MorphMany (not generic)
  - unreadNotifications() returns Builder
- Added types/Collections/helpers.php
- Added types/Pagination/Paginator.php
- Fixed getIterator() return types to match Hypervel implementations
@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.

Add AfterEachTestExtension and AfterEachTestSubscriber to automatically
call Mockery::close() after each test method, eliminating the need for
explicit m::close() calls in individual test tearDown methods.

This follows Laravel's approach from PR #58278, centralizing Mockery
cleanup via PHPUnit's event system instead of scattered manual calls.

Changes:
- Add tests/AfterEachTestExtension.php and AfterEachTestSubscriber.php
- Register extension in phpunit.xml.dist
- Remove redundant m::close() calls from ~50 test files
- Keep intentional m::close() calls that trigger expected exceptions
Port testing features from testbench-testing-features branch:

- Add contract interfaces for attribute lifecycle hooks (Actionable, Invokable,
  Resolvable, BeforeAll, AfterAll, BeforeEach, AfterEach, TestingFeature)
- Add test attributes (#[WithConfig], #[DefineEnvironment], #[DefineRoute],
  #[DefineDatabase], #[RequiresEnv], #[ResetRefreshDatabaseState], #[WithMigration])
- Add AttributeParser for parsing class/method attributes with caching
- Add InteractsWithTestCase trait for lifecycle management and attribute execution
- Add HandlesAttributes trait for attribute parsing helpers
- Add FeaturesCollection for deferred callback management
- Add testbench concerns (CreatesApplication, HandlesDatabases, HandlesRoutes)
- Add comprehensive test coverage for all new functionality
Call defineEnvironment() in refreshApplication() to allow test cases
to configure the application environment before it fully initializes.
- Add trait uses: CreatesApplication, HandlesDatabases, HandlesRoutes,
  HandlesAttributes, InteractsWithTestCase
- Add route setup in afterApplicationCreated callback
- Execute BeforeEach/AfterEach attributes in coroutine context
- Add defineEnvironment() to register package providers/aliases
- Add reloadApplication() helper method
- Add setUpBeforeClass/tearDownAfterClass for BeforeAll/AfterAll attributes
- Use getPackageProviders() for SanctumServiceProvider registration
- Use defineEnvironment() for configuration setup
- Use defineRoutes(Router $router) for route definition
- Simplify setUp() to only call createUsersTable()

This follows the testbench pattern where providers, environment,
and routes are configured via dedicated hook methods.
Replace Hypervel\Foundation\Contracts\Application with
Hypervel\Contracts\Foundation\Application to match the
hyperf-decouple refactoring.
- Move tests from tests/Database/Integration/ to tests/Integration/Database/
- Use DatabaseMigrations trait with afterRefreshingDatabase() for table creation
- Add RequiresDatabase attribute for driver-specific test skipping
- Update database config to use standard DB_* env vars
- Add databases.yml workflow for MySQL/PostgreSQL/MariaDB CI testing
- Fix exception propagation in runInCoroutine for proper PHPUnit skip handling
- Remove @group annotations in favor of directory-based test organization
- Remove hyperf/database and hyperf/db-connection from all composer.json files
- Add hypervel/database dependency to packages that use it (auth, foundation, nested-set, notifications, queue, sanctum)
- Fix WithMigration.php to use Hypervel\Database\Migrations\Migrator
- Clean up HasGlobalScopesTest.php (remove unused Hyperf reference)
- Add RunTestsInCoroutine trait to DatabaseTestCase (required for Swoole PDO hooks)
- Refactor ScopesTest to seed data in test methods, not setUp()
- Laravel pattern: setUp() for non-DB setup, afterRefreshingDatabase() for schema, test methods for seeding
Tests create tables in afterRefreshingDatabase() per Laravel pattern.
- Rename workflow to 'databases' (matches Laravel convention)
- Update checkout action to v6
- Add timeout-minutes: 5 to all jobs
- Add fail-fast strategy to all jobs
- Add SQLite job for tests/Integration/Database/Sqlite
- Add mariadb connection config with proper driver
- Update MariaDB job to use DB_CONNECTION=mariadb
- Update tests.yml checkout to v6
Tests now run inside coroutine context via RunTestsInCoroutine trait,
so the explicit run() wrapper is no longer needed. Tests use go() directly.
Test already runs in coroutine context via RunTestsInCoroutine trait.
SQLite is always available and the tests only verify connection switching,
not querying the alternate database. Fixes MySQL/MariaDB test failures.
Swoole's MySQL PDO hooks require database operations to run inside a
coroutine context. The RunTestsInCoroutine trait wraps test methods
in coroutines, but setUp() runs outside this context. Moving seeding
to a seedProducts() helper called within each test method fixes the
"API must be called in the coroutine" crash on MySQL/MariaDB.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants