feat: Add #[UseFactory] attribute and per-class model name resolvers
#329
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.
Summary
This PR ports Laravel's
#[UseFactory]attribute to Hypervel, allowing models to declare their factory class using a PHP attribute instead of a static$factoryproperty or convention-based resolution.Additionally, this PR ports Laravel's per-factory-class
$modelNameResolverspattern to fix a potential race condition in concurrent environments when usingguessModelNamesUsing().Part 1:
#[UseFactory]AttributeNew Files
src/core/src/Database/Eloquent/Attributes/UseFactory.php- The attribute class that can be applied to model classes to declare their factoryModified Files
src/core/src/Database/Eloquent/Factories/HasFactory.php- AddednewFactory()andgetUseFactoryAttribute()methods, updatedfactory()to use the new resolution orderUsage
The model will now use
PostFactorywhen callingPost::factory():Factory Resolution Order
$factoryproperty -static::$factoryon the model takes precedence#[UseFactory]attribute - Checked if no static property existsFactory::factoryForModel()if no attributePart 2: Per-Factory-Class Model Name Resolvers
The Problem
The previous implementation used a single static
$modelNameResolverfor all factory classes:In concurrent environments (like Swoole/Hypervel), this creates a race condition where one coroutine's factory resolution could be corrupted by another coroutine setting the global resolver.
The Solution
Ported Laravel's
$modelNameResolverspattern which stores resolvers per-factory-class:Modified Files
src/core/src/Database/Eloquent/Factories/Factory.php:$modelNameResolversarray (per-factory-class resolvers)$modelNameResolver(kept for backwards compatibility)guessModelNamesUsing()to store resolvers per-classmodelName()to check per-class resolvers firstflushState()method to reset all static state (for testing)Model Name Resolution Order
$modelproperty on the factory$modelNameResolvers[static::class])$modelNameResolvers[self::class]) - global fallback$modelNameResolver(backwards compatibility)Test Files
tests/Core/Database/Eloquent/Factories/FactoryTest.php:Factory::flushState()to tearDown for proper test isolationUseFactoryattribute and per-class resolversNew Test Cases
testUseFactoryAttributeResolvesFactorytestUseFactoryAttributeResolvesCorrectModelNameguessModelNamesUsing()testUseFactoryAttributeWorksWithCountfactory(3)->make()testStaticFactoryPropertyTakesPrecedenceOverUseFactoryAttribute$factoryproperty beats attributetestModelWithoutUseFactoryFallsBackToConventiontestPerClassModelNameResolverIsolationtestPerClassResolversDoNotInterferetestFlushStateResetsAllResolversflushState()resets everythingTest Plan
#[UseFactory]resolves factory from attribute$factoryproperty takes precedence over attributeFactory::flushState()resets all static state