Skip to content

Commit

Permalink
ECS-427 Added WithSetPkTrait
Browse files Browse the repository at this point in the history
  • Loading branch information
valerialukinykh committed Oct 14, 2024
1 parent a89487c commit 6917d1d
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 0 deletions.
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,55 @@ $this->faker->dateMore()
$this->faker->modelId() // return unsigned bit integer value
```

## Additional traits

### WithSetPkTrait

If your model has unique index consisting of multiple fields, WithSetPkTrait trait should be used to ensure generated values for these fields are unique.

In order for trait to work, you have to define methods `state`, `sequence` and `generatePk` and include `generatePk` call in `definition`.
Following is the example of a factory ('client_id' and 'location_id' are fields forming unique index):

```php
class ClientAmountFactory extends BaseModelFactory
{
use WithSetPkTrait;

protected $model = ClientAmount::class;

public function definition(): array
{
return array_merge($this->generatePk(), [
'amount' => $this->faker->numberBetween(1, 1_000_000),
]);
}

public function state(mixed $state): static // Override of Laravel Eloquent Factory method
{
return $this->stateSetPk($state, ['client_id', 'location_id']);
}

public function sequence(...$sequence): static // Override of Laravel Eloquent Factory method
{
return $this->stateSetPk($sequence, ['client_id', 'location_id'], true);
}

protected function generatePk(?int $clientId = null, ?string $locationId = null): array
{
$clientIdFormat = $clientId ?: '\d{10}';
$locationIdFormat = $locationId ?: '[0-9]{1,10}';

$unique = $this->faker->unique()->regexify("/^{$clientIdFormat}_{$locationIdFormat}");

$uniqueArr = explode('_', $unique);

return [
'client_id' => (int)$uniqueArr[0],
'location_id' => $uniqueArr[1],
];
}
}
```

## Parent classes

Expand Down
90 changes: 90 additions & 0 deletions src/WithSetPkTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

namespace Ensi\LaravelTestFactories;

use Illuminate\Database\Eloquent\Factories\Sequence;

trait WithSetPkTrait
{
protected bool $hasValue = false;
protected array $pkValues = [];

protected function stateSetPk(mixed $state, array $keys, bool $isSequence = false): static
{
if ($isSequence) {
$newSequence = new Sequence(...$state);
$variables = [];

foreach ($state as $sequenceState) {
$variables[] = $this->getVariables($sequenceState, $keys);
}

if ($this->hasValue) {
return $this->getSequenceState($variables, $newSequence)->state($newSequence);
}

return parent::state($newSequence);
}

if (is_array($state)) {
$variables = $this->getVariables($state, $keys);

if ($this->hasValue) {
return $this->state(function () use ($variables) {
return $this->generatePk(...$variables);
})
->state($state);
}
}

return parent::state($state);
}

protected function getVariables(mixed &$state, array $keys): array
{
$variables = [];
foreach ($keys as $key) {
if (array_key_exists($key, $state)) {
$variables[] = $state[$key];
unset($state[$key]);
$this->hasValue = true;
} else {
$variables[] = null;
}
}

return $variables;
}

protected function getSequenceState(array $variables, Sequence $sequence): self
{
return $this->state(function () use ($variables, $sequence) {
$values = $variables[$sequence->index % $sequence->count] ?? [null, null];

$isNewSequence = true;
foreach ($this->pkValues as $sequenceValues) {
// Skip all sequences that are applied after current one
if ($sequenceValues == $variables) {
$isNewSequence = false;

break;
}

// Apply previously created sequence to current one
foreach ($sequenceValues[$sequence->index % count($sequenceValues)] as $pkKey => $pkValue) {
if ($values[$pkKey]) {
continue;
}

$values[$pkKey] = $pkValue;
}
}

if ($isNewSequence) {
$this->pkValues[] = $variables;
}

return $this->generatePk(...$values);
});
}
}
16 changes: 16 additions & 0 deletions tests/Stubs/TestObjectModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Ensi\LaravelTestFactories\Tests\Stubs;

use Illuminate\Database\Eloquent\Model;

/**
* @property int $client_id PK field
* @property string $location_id PK field
*
* @property int $amount
*/

class TestObjectModel extends Model
{
}
45 changes: 45 additions & 0 deletions tests/Stubs/TestObjectWithSetPkTraitFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Ensi\LaravelTestFactories\Tests\Stubs;

use Ensi\LaravelTestFactories\BaseModelFactory;
use Ensi\LaravelTestFactories\WithSetPkTrait;

class TestObjectWithSetPkTraitFactory extends BaseModelFactory
{
use WithSetPkTrait;

protected $model = TestObjectModel::class;

public function definition(): array
{
return array_merge($this->generatePk(), [
'amount' => $this->faker->numberBetween(1, 1_000_000),
]);
}

public function state(mixed $state): static
{
return $this->stateSetPk($state, ['client_id', 'location_id']);
}

public function sequence(...$sequence): static
{
return $this->stateSetPk($sequence, ['client_id', 'location_id'], true);
}

protected function generatePk(?int $clientId = null, ?string $locationId = null): array
{
$clientIdFormat = $clientId ?: '\d{10}';
$locationIdFormat = $locationId ?: '[0-9]{1,10}';

$unique = $this->faker->unique()->regexify("/^{$clientIdFormat}_{$locationIdFormat}");

$uniqueArr = explode('_', $unique);

return [
'client_id' => (int)$uniqueArr[0],
'location_id' => $uniqueArr[1],
];
}
}
93 changes: 93 additions & 0 deletions tests/WithSetPkTraitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

use Ensi\LaravelTestFactories\Tests\Stubs\TestObjectModel;
use Ensi\LaravelTestFactories\Tests\Stubs\TestObjectWithSetPkTraitFactory;

test('TestObjectWithSetPkTraitFactory create', function () {
/** @var TestObjectModel $result */
$result = TestObjectWithSetPkTraitFactory::new()->make(['client_id' => 1]);

expect($result->client_id)->toEqual(1);
});

test('TestObjectWithSetPkTraitFactory create with sequence', function () {
$result = TestObjectWithSetPkTraitFactory::new()
->count(10)
->sequence(
['amount' => 100],
['amount' => 200],
)
->make();

expect($result->filter(fn (TestObjectModel $object) => $object->amount == 200)->count())->toEqual(5);
});

test('TestObjectWithSetPkTraitFactory create with sequence affecting PK', function () {
$result = TestObjectWithSetPkTraitFactory::new()
->count(10)
->sequence(
['client_id' => 1],
['client_id' => 2],
)
->make();

expect($result->filter(fn (TestObjectModel $object) => $object->client_id == 2)->count())->toEqual(5);
});

test('TestObjectWithSetPkTraitFactory create with multiple sequences', function () {
$result = TestObjectWithSetPkTraitFactory::new()
->count(10)
->sequence(
['client_id' => 1],
['client_id' => 2],
)
->sequence(
['amount' => 100],
['amount' => 200],
['amount' => 300]
)
->make();

expect($result->filter(fn (TestObjectModel $object) => $object->client_id == 2)->count())->toEqual(5);
expect($result->filter(fn (TestObjectModel $object) => $object->amount == 100)->count())->toEqual(4);
});

test('TestObjectWithSetPkTraitFactory create with multiple sequences affecting PK', function () {
$result = TestObjectWithSetPkTraitFactory::new()
->count(10)
->sequence(
['client_id' => 1],
['client_id' => 2],
)
->sequence(
['location_id' => 10],
['location_id' => 20],
['location_id' => 30],
['location_id' => 40],
['location_id' => 50]
)
->make();

expect($result->filter(fn (TestObjectModel $object) => $object->client_id == 2)->count())->toEqual(5);
expect($result->filter(fn (TestObjectModel $object) => $object->location_id == 10)->count())->toEqual(2);
});

test('TestObjectWithSetPkTraitFactory create with multiple sequences affecting PK - reverse count', function () {
$result = TestObjectWithSetPkTraitFactory::new()
->count(10)
->sequence(
['client_id' => 1],
['client_id' => 2],
['client_id' => 3],
['client_id' => 4],
['client_id' => 5],
)
->sequence(
['location_id' => 10],
['location_id' => 20],
)
->make();

expect($result->filter(fn (TestObjectModel $object) => $object->client_id == 2)->count())->toEqual(2);
expect($result->filter(fn (TestObjectModel $object) => $object->location_id == 10)->count())->toEqual(5);
});

0 comments on commit 6917d1d

Please sign in to comment.