webmozarts/console-parallelization
Parallelize Symfony Console commands using multiple processes. A main process distributes items to child workers, restarts workers after segments to avoid slowdown, and supports batching with hooks for setup/teardown (e.g., DB flush) for faster bulk jobs.
Installation: Add the package via Composer:
composer require webmozarts/console-parallelization
Extend ParallelCommand: Create a command extending ParallelCommand or using the Parallelization trait:
use Webmozarts\Console\Parallelization\ParallelCommand;
class ImportUsersCommand extends ParallelCommand
{
protected static $defaultName = 'import:users';
protected function fetchItems(): iterable
{
// Return string items (e.g., JSON-encoded data)
yield '{"id": 1, "name": "John"}';
yield '{"id": 2, "name": "Jane"}';
}
protected function runSingleCommand(string $item): void
{
$data = json_decode($item);
// Process $data (e.g., save to DB)
}
protected function getItemName(?int $count): string
{
return $count ? 'users' : 'user';
}
}
Run the Command:
php artisan import:users --main-processphp artisan import:users --processes=4 (default: auto-detects CPU cores).Fetch Items: Implement fetchItems() to return an iterable of string items (e.g., JSON, CSV lines).
protected function fetchItems(): iterable
{
$file = fopen('users.csv', 'r');
while (($line = fgets($file)) !== false) {
yield trim($line);
}
fclose($file);
}
Process Items: Override runSingleCommand() to handle each item.
protected function runSingleCommand(string $item): void
{
$user = json_decode($item);
User::create($user->name, $user->email);
}
Batch Processing: Use hooks for batch-level operations (e.g., DB flushes).
protected function runAfterBatch(array $items): void
{
DB::commit(); // Flush transactions
}
Configuration: Customize segment/batch sizes via configureParallelExecutableFactory().
protected function configureParallelExecutableFactory(
ParallelExecutorFactory $factory,
InputInterface $input,
OutputInterface $output
): ParallelExecutorFactory {
return $factory
->withSegmentSize(1000) // Process 1000 items per child process
->withBatchSize(100); // Group 100 items per batch
}
protected function runSingleCommand(string $item): void
{
$userRepository = $this->getContainer()->get(UserRepository::class);
// ...
}
ResetServiceErrorHandler to reset the container on failures (avoids Doctrine EM issues).createLogger() for distributed process logs.
protected function createLogger(): LoggerInterface
{
return new Logger('parallel_import');
}
Item Format:
json_encode() or base64_encode() for complex data.yield base64_encode(json_encode($item));
Non-Rewindable Generators:
fetchItems() must return a rewindable iterable (e.g., array or Iterator).Iterator:protected function fetchItems(): array
{
return iterator_to_array($this->generateItems());
}
Service Staleness:
$this->getContainer()->get(MyService::class);
Working Directory:
chdir() in hooks.Memory Leaks:
--processes=1 first.--child to test child process logic:
php artisan import:users --child < item.txt
protected function runBeforeBatch(): void
{
$this->getOutput()->writeln('Starting batch...');
}
ulimit -u for max processes. Adjust --processes accordingly.->withPhpExecutable(['php82', '-n']) // Use PHP 8.2 with no config
protected function runBeforeFirstCommand(): void
{
$this->getOutput()->writeln('Preparing resources...');
}
protected function getSegmentSize(): int
{
return $this->getItemCount() / $this->getProcessCount();
}
50 (default) and adjust based on memory usage.--processes=N where N is ≤ CPU cores (e.g., N=4 for 8-core machines).
```markdown
### Laravel-Specific Notes
1. **Artisan Integration**:
- Register commands in `app/Console/Kernel.php`:
```php
protected $commands = [
Commands\ImportUsersCommand::class,
];
app() instead of $this->getContainer():$userRepository = app(UserRepository::class);
ParallelCommand by overriding fetchItems() with a small dataset:$command = new ImportUsersCommand();
$command->fetchItems = fn() => ['item1', 'item2'];
How can I help you explore Laravel packages today?