Skip to content

Commit b8e33e1

Browse files
committed
Add ability to add dynamic factory states on classes using HasFactory
1 parent b0067be commit b8e33e1

File tree

5 files changed

+207
-3
lines changed

5 files changed

+207
-3
lines changed

readme.md

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ The `HasFactory` trait is applied directly to the class you would like to genera
8686
To use the `HasFactory` trait, you must implement the `toFactoryInstance` and `getFactoryDefinition` methods:
8787

8888
> [!note]
89-
> The `HasFactory` trait does not provide you the capability of defining state methods or callbacks.
90-
> If you need this functionality, you should define a separate `Factory` class instead.
89+
> The `HasFactory` trait supports dynamic state methods. You can define state methods in the format `get{StateName}State` and call them via `Class::factory()->stateName()->make()`.
90+
> For more complex factory logic or callbacks, you should define a separate `Factory` class instead.
9191
9292
```php
9393
namespace App\Data;
@@ -141,6 +141,83 @@ Once implemented, you may call the `Reservation::factory()` method to create a n
141141
$factory = Reservation::factory();
142142
```
143143

144+
#### Dynamic State Methods
145+
146+
The `HasFactory` trait supports dynamic state methods. You can define state methods in your class using the format `get{StateName}State` and call them dynamically on the factory:
147+
148+
```php
149+
namespace App\Data;
150+
151+
use DateTime;
152+
use Faker\Generator;
153+
use DirectoryTree\Dummy\HasFactory;
154+
155+
class Reservation
156+
{
157+
use HasFactory;
158+
159+
public function __construct(
160+
public string $name,
161+
public string $email,
162+
public DateTime $datetime,
163+
public string $status = 'pending',
164+
public string $type = 'standard',
165+
) {}
166+
167+
protected static function toFactoryInstance(array $attributes): self
168+
{
169+
return new static(
170+
$attributes['name'],
171+
$attributes['email'],
172+
$attributes['datetime'],
173+
$attributes['status'] ?? 'pending',
174+
$attributes['type'] ?? 'standard',
175+
);
176+
}
177+
178+
protected static function getFactoryDefinition(Generator $faker): array
179+
{
180+
return [
181+
'name' => $faker->name(),
182+
'email' => $faker->email(),
183+
'datetime' => $faker->dateTime(),
184+
];
185+
}
186+
187+
// Dynamic state methods
188+
public static function getConfirmedState(): array
189+
{
190+
return ['status' => 'confirmed'];
191+
}
192+
193+
public static function getPremiumState(): array
194+
{
195+
return [
196+
'type' => 'premium',
197+
'status' => 'confirmed',
198+
];
199+
}
200+
201+
public static function getCancelledState(): array
202+
{
203+
return ['status' => 'cancelled'];
204+
}
205+
}
206+
```
207+
208+
You can then use these state methods dynamically:
209+
210+
```php
211+
// Create a confirmed reservation
212+
$confirmed = Reservation::factory()->confirmed()->make();
213+
214+
// Create a premium reservation
215+
$premium = Reservation::factory()->premium()->make();
216+
217+
// Chain multiple states
218+
$premiumCancelled = Reservation::factory()->premium()->cancelled()->make();
219+
```
220+
144221
### Class Factory
145222

146223
If you need more control over the dummy data generation process, you may use the `Factory` class.

src/Factory.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace DirectoryTree\Dummy;
44

5+
use BadMethodCallException;
56
use Closure;
67
use Faker\Factory as FakerFactory;
78
use Faker\Generator;
@@ -26,6 +27,7 @@ public function __construct(
2627
protected ?Closure $using = null,
2728
protected Collection $states = new Collection,
2829
protected Collection $afterMaking = new Collection,
30+
protected ?string $class = null,
2931
) {
3032
$this->faker = $this->newFaker();
3133
}
@@ -270,6 +272,7 @@ protected function newInstance(array $arguments = []): static
270272
'using' => $this->using,
271273
'states' => $this->states,
272274
'afterMaking' => $this->afterMaking,
275+
'usingClass' => $this->class,
273276
], $arguments)));
274277
}
275278

@@ -292,4 +295,34 @@ protected function newFaker(): Generator
292295

293296
return FakerFactory::create();
294297
}
298+
299+
/**
300+
* Handle dynamic state method calls.
301+
*/
302+
public function __call(string $method, array $parameters): static
303+
{
304+
if (! $this->class) {
305+
throw new BadMethodCallException('Cannot call state methods on a factory without a using class.');
306+
}
307+
308+
if (! method_exists($this->class, $stateMethod = $this->getStateMethod($method))) {
309+
throw new BadMethodCallException("Method [{$method}] does not exist on [{$this->class}].");
310+
}
311+
312+
if (! is_callable([$this->class, $stateMethod])) {
313+
throw new BadMethodCallException("Method [{$method}] is not callable on [{$this->class}].");
314+
}
315+
316+
return $this->state(
317+
call_user_func([$this->class, $stateMethod], $parameters)
318+
);
319+
}
320+
321+
/**
322+
* Get the state method name for the given method name.
323+
*/
324+
protected function getStateMethod(string $method): string
325+
{
326+
return 'get'.ucfirst($method).'State';
327+
}
295328
}

src/HasFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function (Generator $faker, array $attributes) {
2525
*/
2626
protected static function newFactory(array $attributes): Factory
2727
{
28-
return Factory::new($attributes);
28+
return (new Factory(class: static::class))->state($attributes);
2929
}
3030

3131
/**
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace DirectoryTree\Dummy\Tests\Fixtures;
4+
5+
use DirectoryTree\Dummy\Data;
6+
use DirectoryTree\Dummy\HasFactory;
7+
use Faker\Generator;
8+
9+
class HasFactoryWithStatesStub
10+
{
11+
use HasFactory;
12+
13+
protected static function toFactoryInstance(array $attributes): Data
14+
{
15+
return new Data($attributes);
16+
}
17+
18+
protected static function getFactoryDefinition(Generator $faker): array
19+
{
20+
return [
21+
'name' => $faker->name(),
22+
'email' => $faker->email(),
23+
'role' => 'user',
24+
'status' => 'active',
25+
];
26+
}
27+
28+
/**
29+
* Admin state method.
30+
*/
31+
public static function getAdminState(): array
32+
{
33+
return [
34+
'role' => 'admin',
35+
'name' => 'Admin User',
36+
'email' => '[email protected]',
37+
];
38+
}
39+
40+
/**
41+
* Inactive state method.
42+
*/
43+
public static function getInactiveState(): array
44+
{
45+
return [
46+
'status' => 'inactive',
47+
];
48+
}
49+
50+
/**
51+
* Premium state method.
52+
*/
53+
public static function getPremiumState(): array
54+
{
55+
return [
56+
'role' => 'premium',
57+
'subscription' => 'premium',
58+
];
59+
}
60+
}

tests/HasFactoryTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use DirectoryTree\Dummy\Data;
44
use DirectoryTree\Dummy\Tests\Fixtures\HasFactoryInstanceStub;
55
use DirectoryTree\Dummy\Tests\Fixtures\HasFactoryStub;
6+
use DirectoryTree\Dummy\Tests\Fixtures\HasFactoryWithStatesStub;
67

78
it('can generate fake data instance', function () {
89
$instance = HasFactoryStub::factory()->make();
@@ -25,3 +26,36 @@
2526
expect($instance)->toBeInstanceOf(HasFactoryInstanceStub::class);
2627
expect($instance->attributes)->toHaveKeys(['name', 'email']);
2728
});
29+
30+
it('can use dynamic state methods', function () {
31+
$instance = HasFactoryWithStatesStub::factory()->admin()->make();
32+
33+
expect($instance)->toBeInstanceOf(Data::class);
34+
expect($instance->role)->toBe('admin');
35+
expect($instance->name)->toBe('Admin User');
36+
expect($instance->email)->toBe('[email protected]');
37+
});
38+
39+
it('can chain multiple dynamic state methods', function () {
40+
$instance = HasFactoryWithStatesStub::factory()->admin()->inactive()->make();
41+
42+
expect($instance)->toBeInstanceOf(Data::class);
43+
expect($instance->role)->toBe('admin');
44+
expect($instance->name)->toBe('Admin User');
45+
expect($instance->email)->toBe('[email protected]');
46+
expect($instance->status)->toBe('inactive');
47+
});
48+
49+
it('can use dynamic state methods with premium state', function () {
50+
$instance = HasFactoryWithStatesStub::factory()->premium()->make();
51+
52+
expect($instance)->toBeInstanceOf(Data::class);
53+
expect($instance->role)->toBe('premium');
54+
expect($instance->subscription)->toBe('premium');
55+
});
56+
57+
it('throws exception for non-existent state methods', function () {
58+
expect(function () {
59+
HasFactoryWithStatesStub::factory()->nonExistentState()->make();
60+
})->toThrow(\BadMethodCallException::class);
61+
});

0 commit comments

Comments
 (0)