Skip to content

Conversation

@binaryfire
Copy link
Contributor

@binaryfire binaryfire commented Dec 31, 2025

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 $factory property or convention-based resolution.

Additionally, this PR ports Laravel's per-factory-class $modelNameResolvers pattern to fix a potential race condition in concurrent environments when using guessModelNamesUsing().


Part 1: #[UseFactory] Attribute

New Files

  • src/core/src/Database/Eloquent/Attributes/UseFactory.php - The attribute class that can be applied to model classes to declare their factory

Modified Files

  • src/core/src/Database/Eloquent/Factories/HasFactory.php - Added newFactory() and getUseFactoryAttribute() methods, updated factory() to use the new resolution order

Usage

use Hypervel\Database\Eloquent\Attributes\UseFactory;
use Hypervel\Database\Eloquent\Factories\HasFactory;

#[UseFactory(PostFactory::class)]
class Post extends Model
{
    use HasFactory;
}

The model will now use PostFactory when calling Post::factory():

Post::factory()->create(); // Uses PostFactory
Post::factory(3)->make();  // Creates 3 Post instances using PostFactory

Factory Resolution Order

  1. Static $factory property - static::$factory on the model takes precedence
  2. #[UseFactory] attribute - Checked if no static property exists
  3. Convention-based resolution - Falls back to Factory::factoryForModel() if no attribute

Part 2: Per-Factory-Class Model Name Resolvers

The Problem

The previous implementation used a single static $modelNameResolver for all factory classes:

// Before: Global resolver shared by all factories
Factory::guessModelNamesUsing(fn () => Post::class);
// This would affect ALL factories, not just PostFactory

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 $modelNameResolvers pattern which stores resolvers per-factory-class:

// After: Each factory class gets its own resolver
PostFactory::guessModelNamesUsing(fn () => Post::class);
CommentFactory::guessModelNamesUsing(fn () => Comment::class);
// Resolvers are isolated - no interference

Modified Files

  • src/core/src/Database/Eloquent/Factories/Factory.php:
    • Added $modelNameResolvers array (per-factory-class resolvers)
    • Deprecated single $modelNameResolver (kept for backwards compatibility)
    • Updated guessModelNamesUsing() to store resolvers per-class
    • Updated modelName() to check per-class resolvers first
    • Added flushState() method to reset all static state (for testing)

Model Name Resolution Order

  1. Explicit $model property on the factory
  2. Per-class resolver for the specific factory class ($modelNameResolvers[static::class])
  3. Per-class resolver for base Factory class ($modelNameResolvers[self::class]) - global fallback
  4. Deprecated single $modelNameResolver (backwards compatibility)
  5. Convention-based resolution

Test Files

  • tests/Core/Database/Eloquent/Factories/FactoryTest.php:
    • Added Factory::flushState() to tearDown for proper test isolation
    • Added 7 new test cases for UseFactory attribute and per-class resolvers

New Test Cases

Test Description
testUseFactoryAttributeResolvesFactory Verifies attribute resolves correct factory class
testUseFactoryAttributeResolvesCorrectModelName Verifies factory knows its model via guessModelNamesUsing()
testUseFactoryAttributeWorksWithCount Verifies attribute works with factory(3)->make()
testStaticFactoryPropertyTakesPrecedenceOverUseFactoryAttribute Verifies $factory property beats attribute
testModelWithoutUseFactoryFallsBackToConvention Verifies convention resolution still works
testPerClassModelNameResolverIsolation Verifies resolvers don't cross-contaminate
testPerClassResolversDoNotInterfere Verifies factory-specific resolvers are isolated
testFlushStateResetsAllResolvers Verifies flushState() resets everything

Test Plan

  • #[UseFactory] resolves factory from attribute
  • Static $factory property takes precedence over attribute
  • Attribute fallback to convention-based resolution works
  • Per-class model name resolvers are isolated
  • Factory::flushState() resets all static state

Port Laravel's #[UseFactory] attribute for declarative factory binding.
Also port per-class $modelNameResolvers to fix race conditions in
concurrent environments when using guessModelNamesUsing().
@binaryfire binaryfire changed the title feat: Add UseFactory attribute and per-class model name resolvers feat: Add #[UseFactory] attribute and per-class model name resolvers Dec 31, 2025
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.

1 participant