Skip to content

Commit 800ecf9

Browse files
authored
Merge pull request #1 from DirectoryTree/dynamic-factory-states
Add ability to add dynamic factory states on classes using HasFactory
2 parents b0067be + 3b7311b commit 800ecf9

File tree

5 files changed

+205
-5
lines changed

5 files changed

+205
-5
lines changed

readme.md

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,6 @@ The `HasFactory` trait is applied directly to the class you would like to genera
8585

8686
To use the `HasFactory` trait, you must implement the `toFactoryInstance` and `getFactoryDefinition` methods:
8787

88-
> [!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.
91-
9288
```php
9389
namespace App\Data;
9490

@@ -141,6 +137,83 @@ Once implemented, you may call the `Reservation::factory()` method to create a n
141137
$factory = Reservation::factory();
142138
```
143139

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

146219
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;
@@ -23,6 +24,7 @@ class Factory
2324
*/
2425
public function __construct(
2526
protected ?int $count = null,
27+
protected ?string $class = null,
2628
protected ?Closure $using = null,
2729
protected Collection $states = new Collection,
2830
protected Collection $afterMaking = new Collection,
@@ -267,6 +269,7 @@ protected function newInstance(array $arguments = []): static
267269
{
268270
return new static(...array_values(array_merge([
269271
'count' => $this->count,
272+
'class' => $this->class,
270273
'using' => $this->using,
271274
'states' => $this->states,
272275
'afterMaking' => $this->afterMaking,
@@ -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+
/**
14+
* Admin state method.
15+
*/
16+
public static function getAdminState(): array
17+
{
18+
return [
19+
'role' => 'admin',
20+
'name' => 'Admin User',
21+
'email' => '[email protected]',
22+
];
23+
}
24+
25+
/**
26+
* Inactive state method.
27+
*/
28+
public static function getInactiveState(): array
29+
{
30+
return [
31+
'status' => 'inactive',
32+
];
33+
}
34+
35+
/**
36+
* Premium state method.
37+
*/
38+
public static function getPremiumState(): array
39+
{
40+
return [
41+
'role' => 'premium',
42+
'subscription' => 'premium',
43+
];
44+
}
45+
46+
protected static function toFactoryInstance(array $attributes): Data
47+
{
48+
return new Data($attributes);
49+
}
50+
51+
protected static function getFactoryDefinition(Generator $faker): array
52+
{
53+
return [
54+
'name' => $faker->name(),
55+
'email' => $faker->email(),
56+
'role' => 'user',
57+
'status' => 'active',
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)