Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,34 @@ The console output will mark the seed step as `CONTINUE` if the step it already
Database\Seeders\UserSeeder ................................... 32 ms DONE


### Recovering from Unique Constraints Violations

Sometimes a Seed Step may throw a _Unique Constraints Violation_ exception, which happens when trying to insert a value that already exists on _unique_ column, like primary keys. It's not too common, but it usually happens when a random generator mistakenly repeats a value, like emails or text.

[If transactions are not disabled](#disable-transactions), the Populator will retry the Seed Step again, showing the retry on the console. If the error persists, it will be thrown.

php artisan db:seed

INFO Seeding database.

Database\Seeders\UserSeeder ...................................... RUNNING
~ Seed normal users ............................................. CONTINUE
~ Seed non-authorized users ................................. RETRY UNIQUE
~ Seed non-authorized users ......................................... DONE
Database\Seeders\UserSeeder ................................... 32 ms DONE

You can set the `retryUnique` property of the `SeedStep` attribute to any number of retries from the default `1`. Alternatively, setting it to `false` or `0` will disable it.

```php
use Laragear\Populate\Attributes\SeedStep;

#[SeedStep(retryUnique: false)]
public function bannedUsers
{
// ...
}
```

### Disable transactions

Laragear Populate's Seeders wraps each Seed Step into its own transaction using the default database connection. This means that, when a Seed Step fails, all database operations inside that method are rolled back.
Expand Down
7 changes: 5 additions & 2 deletions src/Attributes/SeedStep.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ class SeedStep
/**
* Create a new Seed Step instance.
*/
public function __construct(public string $as = '', public ?bool $withoutModelEvents = null)
{
public function __construct(
public string $as = '',
public ?bool $withoutModelEvents = null,
public false|int $retryUnique = 1,
) {
//
}
}
3 changes: 3 additions & 0 deletions src/Exceptions/SkipSeeding.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

use RuntimeException;

/**
* @internal
*/
class SkipSeeding extends RuntimeException
{
//
Expand Down
2 changes: 1 addition & 1 deletion src/Pipes/MayLoadPreviousSeeding.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,6 @@ protected function continueFilePath(Seeding $seeding): string
*/
protected function outputContinuation(Seeding $seeding): void
{
$seeding->command?->line('Continuing from previous incomplete seeding.');
$seeding->comment('Continuing from previous incomplete seeding.');
}
}
6 changes: 2 additions & 4 deletions src/Pipes/MaySkipSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,10 @@ protected function callBefore(Seeding $seeding): bool
*/
protected function outputSkipped(Seeding $seeding, SkipSeeding $e): void
{
$seeding->command?->outputComponents()->twoColumnDetail(
'~ '.$seeding->seeder::class, '<fg=blue;options=bold>SKIPPED</>',
);
$seeding->twoColumn('~ '.$seeding->seeder::class, '<fg=blue;options=bold>SKIPPED</>');

if ($e->getMessage()) {
$seeding->command?->comment(" {$e->getMessage()}");
$seeding->comment(" {$e->getMessage()}");
}
}
}
3 changes: 3 additions & 0 deletions src/Pipes/RemoveContinueDataAndFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
use Laragear\Populate\Populator;
use Laragear\Populate\Seeding;

/**
* @internal
*/
class RemoveContinueDataAndFile
{
/**
Expand Down
88 changes: 50 additions & 38 deletions src/Pipes/WrapSeedSteps.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Laragear\Populate\Attributes\SeedStep;
Expand Down Expand Up @@ -41,15 +42,18 @@ public function handle(Seeding $seeding, Closure $next): mixed
$seeding->steps->transform(
function (ReflectionMethod $method) use ($seeding, $withoutEvents): Closure {
return function () use ($seeding, $method, $withoutEvents): void {
[$output, $silent] = $this->parseMethodAttribute($method, $withoutEvents);
$seedStep = $this->parseMethodAttribute($method, $withoutEvents);

if ($this->seedStepAlreadyRan($seeding, $method->name, $output)) {
if ($this->seedStepAlreadyRan($seeding, $method->name, $seedStep)) {
return;
}

$this->parseResult(
$this->handleSeedStep(
$seeding, $method, $output, $seeding->parameters[$method->name] ?? [], $silent
$seeding,
$method,
$seedStep,
$seeding->parameters[$method->name] ?? [],
),
);

Expand All @@ -64,10 +68,10 @@ function (ReflectionMethod $method) use ($seeding, $withoutEvents): Closure {
/**
* Check if the method already ran.
*/
protected function seedStepAlreadyRan(Seeding $seeding, string $method, string $output): bool
protected function seedStepAlreadyRan(Seeding $seeding, string $method, SeedStep $step): bool
{
if (isset($this->data->continue[$seeding->seeder::class][$method])) {
$seeding->command?->outputComponents()->twoColumnDetail("~ $output", '<fg=gray;options=bold>CONTINUE</>');
$seeding->twoColumn("~ $step->as", '<fg=gray;options=bold>CONTINUE</>');

return true;
}
Expand Down Expand Up @@ -96,27 +100,30 @@ protected function parseResult(mixed $result): void
protected function handleSeedStep(
Seeding $seeding,
ReflectionMethod $method,
string $output,
SeedStep $step,
array $parameters,
bool $withoutEvents,
): mixed {
{
try {
$result = $this->runSeedStep($seeding, $method, $parameters, $withoutEvents);
} catch (SkipSeeding $e) {
return $this->outputSeedStepSkipped($seeding, $output, $e);
} catch (Throwable $e) {
$seeding->command?->outputComponents()->twoColumnDetail("! $output", '<fg=red;options=bold>ERROR</>');

throw $e;
}
try {
$result = $this->runSeedStep($seeding, $method, $parameters, $step->withoutModelEvents);
} catch (SkipSeeding $e) {
return $this->outputSeedStepSkipped($seeding, $step, $e);
} catch (UniqueConstraintViolationException $e) {
if ($seeding->seeder->useTransactions && $step->retryUnique > 0) {
$seeding->twoColumn("~ $step->as", '<fg=yellow;options=bold>RETRY UNIQUE</>');

$seeding->command?->outputComponents()->twoColumnDetail(
"~ $output", '<fg=green;options=bold>DONE</>',
);
--$step->retryUnique;

return $this->handleSeedStep($seeding, $method, $step, $seeding->parameters[$method->name] ?? []);
}

return $result;
$this->throwStepError($seeding, $step, $e);
} catch (Throwable $e) {
$this->throwStepError($seeding, $step, $e);
}

$seeding->twoColumn("~ $step->as", '<fg=green;options=bold>DONE</>');

return $result;
}

/**
Expand All @@ -135,12 +142,12 @@ protected function runSeedStep(Seeding $seeding, ReflectionMethod $method, array
if ($silent) {
if (method_exists($seeding->seeder, 'withoutModelEvents')) {
return $seeding->seeder->withoutModelEvents(
fn(): mixed => $seeding->container->call([$seeding->seeder, $method->name], $parameters)
fn(): mixed => $seeding->container->call([$seeding->seeder, $method->name], $parameters),
)();
}

return Model::withoutEvents(
fn(): mixed => $seeding->container->call([$seeding->seeder, $method->name], $parameters)
fn(): mixed => $seeding->container->call([$seeding->seeder, $method->name], $parameters),
);
}

Expand All @@ -149,35 +156,40 @@ protected function runSeedStep(Seeding $seeding, ReflectionMethod $method, array

/**
* Parse the Method Attribute to retrieve the output name and events configuration.
*
* @return array{string, bool}
*/
protected function parseMethodAttribute(ReflectionMethod $method, bool $withoutModelEvents): array
protected function parseMethodAttribute(ReflectionMethod $method, bool $withoutModelEvents): SeedStep
{
if ($attribute = Arr::first($method->getAttributes(SeedStep::class))?->newInstance()) {
return [
$attribute->as ?: Str::ucfirst(Str::snake($method->name, ' ')),
$attribute->withoutModelEvents ?? $withoutModelEvents
];
}
/** @var \Laragear\Populate\Attributes\SeedStep $attribute */
$attribute = Arr::first($method->getAttributes(SeedStep::class))?->newInstance()
?? new SeedStep();

return [Str::ucfirst(Str::snake($method->name, ' ')), $withoutModelEvents];
$attribute->as = $attribute->as ?: Str::ucfirst(Str::snake($method->name, ' '));
$attribute->withoutModelEvents ??= $withoutModelEvents;

return $attribute;
}

/**
* Outputs the Seed Step that was skipped to the console.
*/
protected function outputSeedStepSkipped(Seeding $seeding, string $output, SkipSeeding $e): false
protected function outputSeedStepSkipped(Seeding $seeding, SeedStep $seedStep, SkipSeeding $e): false
{
$seeding->command?->outputComponents()->twoColumnDetail(
"~ $output", '<fg=blue;options=bold>SKIPPED</>',
);
$seeding->twoColumn("~ $seedStep->as", '<fg=blue;options=bold>SKIPPED</>');

if ($e->getMessage()) {
$seeding->command?->comment(" {$e->getMessage()}");
$seeding->comment(" {$e->getMessage()}");
}

return false;
}

/**
* Throws an exception and output in the console when the Seed Step errors.
*/
protected function throwStepError(Seeding $seeding, SeedStep $step, Throwable $e): never
{
$seeding->twoColumn("! $step->as", '<fg=red;options=bold>ERROR</>');

throw $e;
}
}
16 changes: 16 additions & 0 deletions src/Seeding.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,20 @@ public function classFileName(): string
{
return Str::replace('\\', '_', $this->class);
}

/**
* Print two columns in the console.
*/
public function twoColumn(string $first, ?string $second = null): void
{
$this->command?->outputComponents()->twoColumnDetail($first, $second);
}

/**
* Print a comment in the console.
*/
public function comment(string $string): void
{
$this->command?->comment($string);
}
}
2 changes: 1 addition & 1 deletion tests/Pipelines/MayLoadPreviousSeedingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function test_loads_data_if_command_has_option(): void

$command = $this->mock(Command::class, function (MockInterface $mock) {
$mock->expects('option')->with('continue')->andReturnTrue();
$mock->expects('line')->with('Continuing from previous incomplete seeding.');
$mock->expects('comment')->with('Continuing from previous incomplete seeding.');
});

$passable = new Seeding($this->app, $command, new EmptySeeder(), [], class: 'test');
Expand Down
Loading