bentools/cartesian-product
Generate the cartesian product (all combinations) from a multidimensional array with a low-memory iterator. Supports dynamic values via closures that can inspect the partial combination. Iterate results or dump them to an array when needed.
Installation:
composer require bentools/cartesian-product
Requires PHP 8.2+ (check your Laravel project’s php-version in phpunit.xml or .php-version).
First Use Case: Generate all possible combinations of product attributes for a dynamic catalog:
use function BenTools\CartesianProduct\combinations;
$productAttributes = [
'color' => ['red', 'blue', 'green'],
'size' => ['S', 'M', 'L'],
'material' => ['cotton', 'polyester'],
];
foreach (combinations($productAttributes) as $variant) {
// Process each unique product variant (e.g., save to DB, generate SKU)
\Log::info('Variant: ' . json_encode($variant));
}
Key Entry Points:
combinations($array): Core function (returns an iterable).->asArray(): Convert to array (use cautiously for large datasets).->count(): Get total combinations without generation.->filter(): Exclude invalid combinations early.->each(): Transform combinations (e.g., into Eloquent models).Use Case: Generate all valid API response formats or feature flag permutations.
$config = [
'auth' => ['jwt', 'session'],
'rate_limit' => [100, 500, null], // null = unlimited
'cache' => ['redis', 'memcached'],
];
foreach (combinations($config)->filter(fn($c) => $c['rate_limit'] !== null) as $configSet) {
// Store or validate each config permutation
Config::store($configSet);
}
Use Case: Conditional logic for order processing (e.g., discounts based on product combinations).
$rules = [
'discount_tier' => [
fn($c) => $c['total'] > 1000 ? 'premium' : 'standard',
fn($c) => $c['total'] > 500 ? 'gold' : null,
],
'shipping' => ['express', 'standard'],
];
foreach (combinations($rules) as $ruleSet) {
if ($ruleSet['discount_tier'] === 'premium') {
// Apply premium logic
}
}
Use Case: Convert flat arrays into nested Eloquent models.
$attributes = [
'title' => ['Book 1', 'Book 2'],
'author' => ['Author A', 'Author B'],
'genre' => ['Fiction', 'Non-Fiction'],
];
$books = combinations($attributes)
->each(fn($attrs) => Book::create($attrs))
->toArray(); // Assuming custom `toArray()` method
Use Case: Hybrid operations with Laravel’s Collection methods.
$combinations = combinations($data)->asArray();
$filtered = collect($combinations)->where(fn($c) => $c['price'] > 100);
Use Case: Generate test data or reports without memory overload.
$total = combinations($data)->count();
$batchSize = 1000;
for ($i = 0; $i < $total; $i += $batchSize) {
$batch = array_slice(iter_to_array($combinations), $i, $batchSize);
processBatch($batch);
}
// Helper: Convert iterable to array (use sparingly)
function iter_to_array(iterable $iterable): array {
return iterator_to_array($iterable);
}
Service Container Binding: Bind the function to Laravel’s container for dependency injection:
$app->bind('combinations', fn() => fn($data) => \BenTools\CartesianProduct\combinations($data));
Caching Combinations: Cache expensive combinations (e.g., product variants) using Laravel’s cache:
$cacheKey = 'product_variants_' . md5(json_encode($attributes));
$variants = cache()->remember($cacheKey, now()->addHours(1), fn() =>
combinations($attributes)->asArray()
);
Queue Jobs for Large Datasets: Offload combination generation to background jobs:
GenerateVariantsJob::dispatch($attributes)->onQueue('combinatorial');
Database Seeding: Use for seeding multi-dimensional test data:
public function run()
{
$combinations = combinations($this->data)->asArray();
DB::table('test_data')->insert($combinations);
}
API Response Generation: Dynamically generate API response structures:
return response()->json(
combinations($this->responseTemplates)->asArray()
);
Memory Explosion with asArray():
->asArray() loads all combinations into memory at once.foreach) or chunking for large datasets.// Bad: Memory-intensive
$allCombinations = combinations($data)->asArray();
// Good: Memory-efficient
foreach (combinations($data) as $combo) {
// Process one at a time
}
Closure Evaluation Order:
$data = [
'dynamic_value' => function() use (&$cache) {
return $cache ?? ($cache = expensiveCalculation());
},
];
Non-Countable Iterables:
\InvalidArgumentException.\Countable or provide a fallback:
$data = [
'key' => new class implements \Countable {
public function count(): int { return 2; }
public function current() { /* ... */ }
// ... other Iterator methods
},
];
Filtered Count Inaccuracy:
count() on a filtered iterator may return incorrect results (fixed in v2.0.1).iterator_count() for filtered iterators:
$filtered = combinations($data)->filter(fn($c) => $c['value'] > 10);
$count = iterator_count($filtered);
PHP 8.2+ Features:
composer.json and server config).Log Combinations for Inspection:
combinations($data)->each(fn($c) => \Log::debug('Combination:', $c));
Validate Input Structure:
foreach ($data as $key => $values) {
if (!is_iterable($values)) {
throw new \InvalidArgumentException("Key '$key' must be iterable.");
}
}
Profile Memory Usage:
$start = memory_get_usage();
foreach (combinations($data) as $combo) { /* ... */ }
$peak = memory_get_peak_usage();
\Log::info("Memory usage: " . ($peak - $start) / 1024 / 1024 . "MB");
Custom Iterators: Extend the package by creating a decorator:
class CachedCombinations implements \IteratorAggregate {
public function __construct(private iterable $combinations) {}
public function getIterator(): \Traversable {
$cache = [];
foreach ($this->combinations as $combo) {
$cache[] = $combo;
}
return new \ArrayIterator($cache);
}
}
Laravel Service Provider: Add helper methods to the Laravel app:
// app/Providers/AppServiceProvider.php
public function boot() {
app()->singleton('combinations', fn() => fn($data) =>
\BenTools\CartesianProduct\combinations($data)
);
}
Dynamic Value Generation: Use closures to generate values based on prior selections:
$data = [
'
How can I help you explore Laravel packages today?