Skip to content

Commit 6917d1d

Browse files
ECS-427 Added WithSetPkTrait
1 parent a89487c commit 6917d1d

File tree

5 files changed

+293
-0
lines changed

5 files changed

+293
-0
lines changed

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,55 @@ $this->faker->dateMore()
124124
$this->faker->modelId() // return unsigned bit integer value
125125
```
126126

127+
## Additional traits
128+
129+
### WithSetPkTrait
130+
131+
If your model has unique index consisting of multiple fields, WithSetPkTrait trait should be used to ensure generated values for these fields are unique.
132+
133+
In order for trait to work, you have to define methods `state`, `sequence` and `generatePk` and include `generatePk` call in `definition`.
134+
Following is the example of a factory ('client_id' and 'location_id' are fields forming unique index):
135+
136+
```php
137+
class ClientAmountFactory extends BaseModelFactory
138+
{
139+
use WithSetPkTrait;
140+
141+
protected $model = ClientAmount::class;
142+
143+
public function definition(): array
144+
{
145+
return array_merge($this->generatePk(), [
146+
'amount' => $this->faker->numberBetween(1, 1_000_000),
147+
]);
148+
}
149+
150+
public function state(mixed $state): static // Override of Laravel Eloquent Factory method
151+
{
152+
return $this->stateSetPk($state, ['client_id', 'location_id']);
153+
}
154+
155+
public function sequence(...$sequence): static // Override of Laravel Eloquent Factory method
156+
{
157+
return $this->stateSetPk($sequence, ['client_id', 'location_id'], true);
158+
}
159+
160+
protected function generatePk(?int $clientId = null, ?string $locationId = null): array
161+
{
162+
$clientIdFormat = $clientId ?: '\d{10}';
163+
$locationIdFormat = $locationId ?: '[0-9]{1,10}';
164+
165+
$unique = $this->faker->unique()->regexify("/^{$clientIdFormat}_{$locationIdFormat}");
166+
167+
$uniqueArr = explode('_', $unique);
168+
169+
return [
170+
'client_id' => (int)$uniqueArr[0],
171+
'location_id' => $uniqueArr[1],
172+
];
173+
}
174+
}
175+
```
127176

128177
## Parent classes
129178

src/WithSetPkTrait.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
namespace Ensi\LaravelTestFactories;
4+
5+
use Illuminate\Database\Eloquent\Factories\Sequence;
6+
7+
trait WithSetPkTrait
8+
{
9+
protected bool $hasValue = false;
10+
protected array $pkValues = [];
11+
12+
protected function stateSetPk(mixed $state, array $keys, bool $isSequence = false): static
13+
{
14+
if ($isSequence) {
15+
$newSequence = new Sequence(...$state);
16+
$variables = [];
17+
18+
foreach ($state as $sequenceState) {
19+
$variables[] = $this->getVariables($sequenceState, $keys);
20+
}
21+
22+
if ($this->hasValue) {
23+
return $this->getSequenceState($variables, $newSequence)->state($newSequence);
24+
}
25+
26+
return parent::state($newSequence);
27+
}
28+
29+
if (is_array($state)) {
30+
$variables = $this->getVariables($state, $keys);
31+
32+
if ($this->hasValue) {
33+
return $this->state(function () use ($variables) {
34+
return $this->generatePk(...$variables);
35+
})
36+
->state($state);
37+
}
38+
}
39+
40+
return parent::state($state);
41+
}
42+
43+
protected function getVariables(mixed &$state, array $keys): array
44+
{
45+
$variables = [];
46+
foreach ($keys as $key) {
47+
if (array_key_exists($key, $state)) {
48+
$variables[] = $state[$key];
49+
unset($state[$key]);
50+
$this->hasValue = true;
51+
} else {
52+
$variables[] = null;
53+
}
54+
}
55+
56+
return $variables;
57+
}
58+
59+
protected function getSequenceState(array $variables, Sequence $sequence): self
60+
{
61+
return $this->state(function () use ($variables, $sequence) {
62+
$values = $variables[$sequence->index % $sequence->count] ?? [null, null];
63+
64+
$isNewSequence = true;
65+
foreach ($this->pkValues as $sequenceValues) {
66+
// Skip all sequences that are applied after current one
67+
if ($sequenceValues == $variables) {
68+
$isNewSequence = false;
69+
70+
break;
71+
}
72+
73+
// Apply previously created sequence to current one
74+
foreach ($sequenceValues[$sequence->index % count($sequenceValues)] as $pkKey => $pkValue) {
75+
if ($values[$pkKey]) {
76+
continue;
77+
}
78+
79+
$values[$pkKey] = $pkValue;
80+
}
81+
}
82+
83+
if ($isNewSequence) {
84+
$this->pkValues[] = $variables;
85+
}
86+
87+
return $this->generatePk(...$values);
88+
});
89+
}
90+
}

tests/Stubs/TestObjectModel.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Ensi\LaravelTestFactories\Tests\Stubs;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
/**
8+
* @property int $client_id PK field
9+
* @property string $location_id PK field
10+
*
11+
* @property int $amount
12+
*/
13+
14+
class TestObjectModel extends Model
15+
{
16+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Ensi\LaravelTestFactories\Tests\Stubs;
4+
5+
use Ensi\LaravelTestFactories\BaseModelFactory;
6+
use Ensi\LaravelTestFactories\WithSetPkTrait;
7+
8+
class TestObjectWithSetPkTraitFactory extends BaseModelFactory
9+
{
10+
use WithSetPkTrait;
11+
12+
protected $model = TestObjectModel::class;
13+
14+
public function definition(): array
15+
{
16+
return array_merge($this->generatePk(), [
17+
'amount' => $this->faker->numberBetween(1, 1_000_000),
18+
]);
19+
}
20+
21+
public function state(mixed $state): static
22+
{
23+
return $this->stateSetPk($state, ['client_id', 'location_id']);
24+
}
25+
26+
public function sequence(...$sequence): static
27+
{
28+
return $this->stateSetPk($sequence, ['client_id', 'location_id'], true);
29+
}
30+
31+
protected function generatePk(?int $clientId = null, ?string $locationId = null): array
32+
{
33+
$clientIdFormat = $clientId ?: '\d{10}';
34+
$locationIdFormat = $locationId ?: '[0-9]{1,10}';
35+
36+
$unique = $this->faker->unique()->regexify("/^{$clientIdFormat}_{$locationIdFormat}");
37+
38+
$uniqueArr = explode('_', $unique);
39+
40+
return [
41+
'client_id' => (int)$uniqueArr[0],
42+
'location_id' => $uniqueArr[1],
43+
];
44+
}
45+
}

tests/WithSetPkTraitTest.php

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
use Ensi\LaravelTestFactories\Tests\Stubs\TestObjectModel;
4+
use Ensi\LaravelTestFactories\Tests\Stubs\TestObjectWithSetPkTraitFactory;
5+
6+
test('TestObjectWithSetPkTraitFactory create', function () {
7+
/** @var TestObjectModel $result */
8+
$result = TestObjectWithSetPkTraitFactory::new()->make(['client_id' => 1]);
9+
10+
expect($result->client_id)->toEqual(1);
11+
});
12+
13+
test('TestObjectWithSetPkTraitFactory create with sequence', function () {
14+
$result = TestObjectWithSetPkTraitFactory::new()
15+
->count(10)
16+
->sequence(
17+
['amount' => 100],
18+
['amount' => 200],
19+
)
20+
->make();
21+
22+
expect($result->filter(fn (TestObjectModel $object) => $object->amount == 200)->count())->toEqual(5);
23+
});
24+
25+
test('TestObjectWithSetPkTraitFactory create with sequence affecting PK', function () {
26+
$result = TestObjectWithSetPkTraitFactory::new()
27+
->count(10)
28+
->sequence(
29+
['client_id' => 1],
30+
['client_id' => 2],
31+
)
32+
->make();
33+
34+
expect($result->filter(fn (TestObjectModel $object) => $object->client_id == 2)->count())->toEqual(5);
35+
});
36+
37+
test('TestObjectWithSetPkTraitFactory create with multiple sequences', function () {
38+
$result = TestObjectWithSetPkTraitFactory::new()
39+
->count(10)
40+
->sequence(
41+
['client_id' => 1],
42+
['client_id' => 2],
43+
)
44+
->sequence(
45+
['amount' => 100],
46+
['amount' => 200],
47+
['amount' => 300]
48+
)
49+
->make();
50+
51+
expect($result->filter(fn (TestObjectModel $object) => $object->client_id == 2)->count())->toEqual(5);
52+
expect($result->filter(fn (TestObjectModel $object) => $object->amount == 100)->count())->toEqual(4);
53+
});
54+
55+
test('TestObjectWithSetPkTraitFactory create with multiple sequences affecting PK', function () {
56+
$result = TestObjectWithSetPkTraitFactory::new()
57+
->count(10)
58+
->sequence(
59+
['client_id' => 1],
60+
['client_id' => 2],
61+
)
62+
->sequence(
63+
['location_id' => 10],
64+
['location_id' => 20],
65+
['location_id' => 30],
66+
['location_id' => 40],
67+
['location_id' => 50]
68+
)
69+
->make();
70+
71+
expect($result->filter(fn (TestObjectModel $object) => $object->client_id == 2)->count())->toEqual(5);
72+
expect($result->filter(fn (TestObjectModel $object) => $object->location_id == 10)->count())->toEqual(2);
73+
});
74+
75+
test('TestObjectWithSetPkTraitFactory create with multiple sequences affecting PK - reverse count', function () {
76+
$result = TestObjectWithSetPkTraitFactory::new()
77+
->count(10)
78+
->sequence(
79+
['client_id' => 1],
80+
['client_id' => 2],
81+
['client_id' => 3],
82+
['client_id' => 4],
83+
['client_id' => 5],
84+
)
85+
->sequence(
86+
['location_id' => 10],
87+
['location_id' => 20],
88+
)
89+
->make();
90+
91+
expect($result->filter(fn (TestObjectModel $object) => $object->client_id == 2)->count())->toEqual(2);
92+
expect($result->filter(fn (TestObjectModel $object) => $object->location_id == 10)->count())->toEqual(5);
93+
});

0 commit comments

Comments
 (0)