Skip to content

Commit af49ae7

Browse files
authored
Merge pull request #3 from DirectoryTree/codex/laravel-factory-parity
Add Laravel factory parity conveniences
2 parents b90391e + a9807ac commit af49ae7

2 files changed

Lines changed: 149 additions & 8 deletions

File tree

src/Factory.php

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99
use Illuminate\Container\Container;
1010
use Illuminate\Support\Collection;
1111
use Illuminate\Support\Traits\Conditionable;
12+
use Illuminate\Support\Traits\Macroable;
1213

1314
class Factory
1415
{
1516
use Conditionable;
17+
use Macroable {
18+
__call as macroCall;
19+
}
1620

1721
/**
1822
* The current Faker instance.
@@ -35,7 +39,7 @@ public function __construct(
3539
/**
3640
* Get a new dummy factory instance for the given attributes.
3741
*/
38-
public static function new(Closure|array $attributes = []): static
42+
public static function new(callable|array $attributes = []): static
3943
{
4044
return (new static)->state($attributes)->configure();
4145
}
@@ -83,7 +87,7 @@ protected function collect(array $instances = []): mixed
8387
/**
8488
* Get the raw attributes generated by the factory.
8589
*/
86-
public function raw(Closure|array $attributes = []): array
90+
public function raw(callable|array $attributes = []): array
8791
{
8892
if ($this->count === null) {
8993
return $this->state($attributes)->getExpandedAttributes();
@@ -97,7 +101,7 @@ public function raw(Closure|array $attributes = []): array
97101
/**
98102
* Create a collection of classes.
99103
*/
100-
public function make(Closure|array $attributes = []): mixed
104+
public function make(callable|array $attributes = []): mixed
101105
{
102106
if (! empty($attributes)) {
103107
return $this->state($attributes)->make();
@@ -122,6 +126,42 @@ public function make(Closure|array $attributes = []): mixed
122126
return $instances;
123127
}
124128

129+
/**
130+
* Make a single instance of the class.
131+
*/
132+
public function makeOne(callable|array $attributes = []): mixed
133+
{
134+
return $this->count(null)->make($attributes);
135+
}
136+
137+
/**
138+
* Create a collection of classes.
139+
*/
140+
public function makeMany(iterable|int|null $records = null): mixed
141+
{
142+
$records ??= ($this->count ?? 1);
143+
144+
$this->count = null;
145+
146+
if (is_numeric($records)) {
147+
$records = array_fill(0, $records, []);
148+
}
149+
150+
return $this->collect(
151+
(new Collection($records))->map(function ($record) {
152+
return $this->state($record)->make();
153+
})->all()
154+
);
155+
}
156+
157+
/**
158+
* Create a callback that makes the class when invoked.
159+
*/
160+
public function lazy(array $attributes = []): Closure
161+
{
162+
return fn () => $this->make($attributes);
163+
}
164+
125165
/**
126166
* Make an instance of the class with the given attributes.
127167
*/
@@ -179,17 +219,27 @@ protected function expandAttributes(array $definition): array
179219
/**
180220
* Add a new state transformation to the class definition.
181221
*/
182-
public function state(mixed $state): static
222+
public function state(callable|array $state): static
183223
{
184224
return $this->newInstance([
185225
'states' => $this->states->concat([
186-
is_callable($state) ? $state : function () use ($state) {
187-
return $state;
188-
},
226+
is_callable($state) ? $state : fn () => $state,
189227
]),
190228
]);
191229
}
192230

231+
/**
232+
* Prepend a new state transformation to the class definition.
233+
*/
234+
public function prependState(callable|array $state): static
235+
{
236+
return $this->newInstance([
237+
'states' => $this->states->prepend(
238+
is_callable($state) ? $state : fn () => $state,
239+
),
240+
]);
241+
}
242+
193243
/**
194244
* Set a single class attribute.
195245
*/
@@ -242,6 +292,16 @@ public function afterMaking(Closure $callback): static
242292
]);
243293
}
244294

295+
/**
296+
* Remove the "after making" callbacks from the factory.
297+
*/
298+
public function withoutAfterMaking(): static
299+
{
300+
return $this->newInstance([
301+
'afterMaking' => new Collection,
302+
]);
303+
}
304+
245305
/**
246306
* Call the "after making" callbacks for the given class instances.
247307
*/
@@ -299,8 +359,12 @@ protected function newFaker(): Generator
299359
/**
300360
* Handle dynamic state method calls.
301361
*/
302-
public function __call(string $method, array $parameters): static
362+
public function __call(string $method, array $parameters): mixed
303363
{
364+
if (static::hasMacro($method)) {
365+
return $this->macroCall($method, $parameters);
366+
}
367+
304368
if (! $this->class) {
305369
throw new BadMethodCallException('Cannot call state methods on a factory without a using class.');
306370
}

tests/FactoryTest.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@
3434
expect($instance->name)->toBe('foo');
3535
});
3636

37+
it('can accept invokable attribute callbacks', function () {
38+
$instance = FactoryStub::new(new class
39+
{
40+
public function __invoke(array $attributes): array
41+
{
42+
return [
43+
'name' => 'Invokable',
44+
'email' => $attributes['email'],
45+
];
46+
}
47+
})->make();
48+
49+
expect($instance->name)->toBe('Invokable');
50+
});
51+
3752
it('can accept attribute closures', function () {
3853
$instance = FactoryStub::new()->make([
3954
'foo' => function (array $attributes) {
@@ -61,6 +76,30 @@
6176
expect($raws[0])->toHaveKeys(['name', 'email']);
6277
});
6378

79+
it('can make a single instance after a count has been set', function () {
80+
$instance = FactoryStub::new()->count(5)->makeOne();
81+
82+
expect($instance)->toBeInstanceOf(Data::class);
83+
});
84+
85+
it('can make many instances from a number', function () {
86+
$collection = FactoryStub::new()->makeMany(3);
87+
88+
expect($collection)->toBeInstanceOf(Collection::class);
89+
expect($collection)->toHaveCount(3);
90+
});
91+
92+
it('can make many instances from state records', function () {
93+
$collection = FactoryStub::new()->makeMany([
94+
['name' => 'Taylor'],
95+
['name' => 'Nuno'],
96+
]);
97+
98+
expect($collection)->toHaveCount(2);
99+
expect($collection[0]->name)->toBe('Taylor');
100+
expect($collection[1]->name)->toBe('Nuno');
101+
});
102+
64103
it('can accept sequence', function () {
65104
$collection = FactoryStub::new()
66105
->count(3)
@@ -93,6 +132,22 @@
93132
expect($instance->id)->toBe(1);
94133
});
95134

135+
it('can prepend state', function () {
136+
$instance = FactoryStub::new()
137+
->state(['name' => 'Last'])
138+
->prependState(['name' => 'First'])
139+
->make();
140+
141+
expect($instance->name)->toBe('Last');
142+
});
143+
144+
it('can lazily make instances', function () {
145+
$lazy = FactoryStub::new()->lazy(['name' => 'Deferred']);
146+
147+
expect($lazy)->toBeInstanceOf(Closure::class);
148+
expect($lazy()->name)->toBe('Deferred');
149+
});
150+
96151
it('can create custom classes', function () {
97152
$instance = FactoryWithCustomClassStub::new()->make();
98153

@@ -126,3 +181,25 @@
126181

127182
expect($instance->name)->toBe('Custom');
128183
});
184+
185+
it('can remove after making callbacks', function () {
186+
$instance = FactoryWithConfigurationStub::new()
187+
->withoutAfterMaking()
188+
->make(['name' => 'Original']);
189+
190+
expect($instance->name)->toBe('Original');
191+
});
192+
193+
it('can use macros', function () {
194+
FactoryStub::macro('named', function (string $name) {
195+
return $this->state(['name' => $name]);
196+
});
197+
198+
try {
199+
$instance = FactoryStub::new()->named('Macro')->make();
200+
201+
expect($instance->name)->toBe('Macro');
202+
} finally {
203+
FactoryStub::flushMacros();
204+
}
205+
});

0 commit comments

Comments
 (0)