diff --git a/readme.md b/readme.md index 2aeb4d1..a5f397d 100644 --- a/readme.md +++ b/readme.md @@ -85,10 +85,6 @@ The `HasFactory` trait is applied directly to the class you would like to genera To use the `HasFactory` trait, you must implement the `toFactoryInstance` and `getFactoryDefinition` methods: -> [!note] -> The `HasFactory` trait does not provide you the capability of defining state methods or callbacks. -> If you need this functionality, you should define a separate `Factory` class instead. - ```php namespace App\Data; @@ -141,6 +137,83 @@ Once implemented, you may call the `Reservation::factory()` method to create a n $factory = Reservation::factory(); ``` +#### Dynamic State Methods + +The `HasFactory` trait supports defining dynamic state methods. You can define state methods in your class using the format `get{StateName}State` and call them dynamically on the factory: + +```php +namespace App\Data; + +use DateTime; +use Faker\Generator; +use DirectoryTree\Dummy\HasFactory; + +class Reservation +{ + use HasFactory; + + public function __construct( + public string $name, + public string $email, + public DateTime $datetime, + public string $status = 'pending', + public string $type = 'standard', + ) {} + + // Dynamic state methods + public static function getConfirmedState(): array + { + return ['status' => 'confirmed']; + } + + public static function getPremiumState(): array + { + return [ + 'type' => 'premium', + 'status' => 'confirmed', + ]; + } + + public static function getCancelledState(): array + { + return ['status' => 'cancelled']; + } + + protected static function toFactoryInstance(array $attributes): self + { + return new static( + $attributes['name'], + $attributes['email'], + $attributes['datetime'], + $attributes['status'] ?? 'pending', + $attributes['type'] ?? 'standard', + ); + } + + protected static function getFactoryDefinition(Generator $faker): array + { + return [ + 'name' => $faker->name(), + 'email' => $faker->email(), + 'datetime' => $faker->dateTime(), + ]; + } +} +``` + +You can then use these state methods dynamically: + +```php +// Create a confirmed reservation +$confirmed = Reservation::factory()->confirmed()->make(); + +// Create a premium reservation +$premium = Reservation::factory()->premium()->make(); + +// Chain multiple states +$premiumCancelled = Reservation::factory()->premium()->cancelled()->make(); +``` + ### Class Factory If you need more control over the dummy data generation process, you may use the `Factory` class. diff --git a/src/Factory.php b/src/Factory.php index 460ee1f..0ec3950 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -2,6 +2,7 @@ namespace DirectoryTree\Dummy; +use BadMethodCallException; use Closure; use Faker\Factory as FakerFactory; use Faker\Generator; @@ -23,6 +24,7 @@ class Factory */ public function __construct( protected ?int $count = null, + protected ?string $class = null, protected ?Closure $using = null, protected Collection $states = new Collection, protected Collection $afterMaking = new Collection, @@ -267,6 +269,7 @@ protected function newInstance(array $arguments = []): static { return new static(...array_values(array_merge([ 'count' => $this->count, + 'class' => $this->class, 'using' => $this->using, 'states' => $this->states, 'afterMaking' => $this->afterMaking, @@ -292,4 +295,34 @@ protected function newFaker(): Generator return FakerFactory::create(); } + + /** + * Handle dynamic state method calls. + */ + public function __call(string $method, array $parameters): static + { + if (! $this->class) { + throw new BadMethodCallException('Cannot call state methods on a factory without a using class.'); + } + + if (! method_exists($this->class, $stateMethod = $this->getStateMethod($method))) { + throw new BadMethodCallException("Method [{$method}] does not exist on [{$this->class}]."); + } + + if (! is_callable([$this->class, $stateMethod])) { + throw new BadMethodCallException("Method [{$method}] is not callable on [{$this->class}]."); + } + + return $this->state( + call_user_func([$this->class, $stateMethod], $parameters) + ); + } + + /** + * Get the state method name for the given method name. + */ + protected function getStateMethod(string $method): string + { + return 'get'.ucfirst($method).'State'; + } } diff --git a/src/HasFactory.php b/src/HasFactory.php index c1355f9..ed45cc2 100644 --- a/src/HasFactory.php +++ b/src/HasFactory.php @@ -25,7 +25,7 @@ function (Generator $faker, array $attributes) { */ protected static function newFactory(array $attributes): Factory { - return Factory::new($attributes); + return (new Factory(class: static::class))->state($attributes); } /** diff --git a/tests/Fixtures/HasFactoryWithStatesStub.php b/tests/Fixtures/HasFactoryWithStatesStub.php new file mode 100644 index 0000000..4098db0 --- /dev/null +++ b/tests/Fixtures/HasFactoryWithStatesStub.php @@ -0,0 +1,60 @@ + 'admin', + 'name' => 'Admin User', + 'email' => 'admin@example.com', + ]; + } + + /** + * Inactive state method. + */ + public static function getInactiveState(): array + { + return [ + 'status' => 'inactive', + ]; + } + + /** + * Premium state method. + */ + public static function getPremiumState(): array + { + return [ + 'role' => 'premium', + 'subscription' => 'premium', + ]; + } + + protected static function toFactoryInstance(array $attributes): Data + { + return new Data($attributes); + } + + protected static function getFactoryDefinition(Generator $faker): array + { + return [ + 'name' => $faker->name(), + 'email' => $faker->email(), + 'role' => 'user', + 'status' => 'active', + ]; + } +} diff --git a/tests/HasFactoryTest.php b/tests/HasFactoryTest.php index 6278918..478ccc7 100644 --- a/tests/HasFactoryTest.php +++ b/tests/HasFactoryTest.php @@ -3,6 +3,7 @@ use DirectoryTree\Dummy\Data; use DirectoryTree\Dummy\Tests\Fixtures\HasFactoryInstanceStub; use DirectoryTree\Dummy\Tests\Fixtures\HasFactoryStub; +use DirectoryTree\Dummy\Tests\Fixtures\HasFactoryWithStatesStub; it('can generate fake data instance', function () { $instance = HasFactoryStub::factory()->make(); @@ -25,3 +26,36 @@ expect($instance)->toBeInstanceOf(HasFactoryInstanceStub::class); expect($instance->attributes)->toHaveKeys(['name', 'email']); }); + +it('can use dynamic state methods', function () { + $instance = HasFactoryWithStatesStub::factory()->admin()->make(); + + expect($instance)->toBeInstanceOf(Data::class); + expect($instance->role)->toBe('admin'); + expect($instance->name)->toBe('Admin User'); + expect($instance->email)->toBe('admin@example.com'); +}); + +it('can chain multiple dynamic state methods', function () { + $instance = HasFactoryWithStatesStub::factory()->admin()->inactive()->make(); + + expect($instance)->toBeInstanceOf(Data::class); + expect($instance->role)->toBe('admin'); + expect($instance->name)->toBe('Admin User'); + expect($instance->email)->toBe('admin@example.com'); + expect($instance->status)->toBe('inactive'); +}); + +it('can use dynamic state methods with premium state', function () { + $instance = HasFactoryWithStatesStub::factory()->premium()->make(); + + expect($instance)->toBeInstanceOf(Data::class); + expect($instance->role)->toBe('premium'); + expect($instance->subscription)->toBe('premium'); +}); + +it('throws exception for non-existent state methods', function () { + expect(function () { + HasFactoryWithStatesStub::factory()->nonExistentState()->make(); + })->toThrow(BadMethodCallException::class); +});