ozdemir / subset-finder
A simple package to find the subset of a given set.
Fund package maintenance!
n1crack
Requires
- php: ^8.1
- ext-redis: *
- laravel/framework: ^12.0
- psr/log: ^3.0
Requires (Dev)
- infection/infection: ^0.29
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^v10.0
- pestphp/pest: ^3.0
- phpstan/phpstan: ^1.10
README
A powerful and flexible PHP package for efficiently finding subsets within collections based on quantity criteria. Built with Laravel collections and optimized for performance, memory efficiency, and developer experience.
Features
- High Performance: Optimized algorithms with configurable memory limits
- Flexible Configuration: Multiple configuration profiles for different use cases
- Performance Monitoring: Built-in metrics and logging capabilities
- Robust Error Handling: Comprehensive validation and meaningful error messages
- Type Safety: Full PHP 8.1+ type support with strict validation
- Comprehensive Testing: 100% test coverage with Pest PHP
- Laravel Integration: Service provider, facade, and trait support
- Memory Efficient: Optional lazy evaluation for large datasets
Installation
composer require ozdemir/subset-finder
Laravel Integration
The package automatically registers with Laravel. If you need to publish the configuration:
php artisan vendor:publish --tag=subset-finder-config
Quick Start
Basic Usage
use Ozdemir\SubsetFinder\Subset; use Ozdemir\SubsetFinder\SubsetCollection; use Ozdemir\SubsetFinder\SubsetFinder; use Ozdemir\SubsetFinder\SubsetFinderConfig; // Define your collection and subset criteria $collection = collect([ new Product(id: 1, quantity: 11, price: 15), new Product(id: 2, quantity: 6, price: 5), new Product(id: 3, quantity: 6, price: 5), ]); $subsetCollection = new SubsetCollection([ Subset::of([1, 2])->take(5), // Find 5 items from products 1 and 2 Subset::of([3])->take(2), // Find 2 items from product 3 ]); // Create and configure SubsetFinder $config = new SubsetFinderConfig( idField: 'id', quantityField: 'quantity', sortField: 'price', // Sort by price (ascending) sortDescending: false ); $subsetFinder = new SubsetFinder($collection, $subsetCollection, $config); $subsetFinder->solve(); // Get results $foundSubsets = $subsetFinder->getFoundSubsets(); $remaining = $subsetFinder->getRemaining(); $maxSubsets = $subsetFinder->getSubsetQuantity();
Using Configuration Profiles
// For large datasets (512MB memory, lazy evaluation enabled) $subsetFinder = new SubsetFinder( $collection, $subsetCollection, SubsetFinderConfig::forLargeDatasets() ); // For performance (64MB memory, lazy evaluation disabled) $subsetFinder = new SubsetFinder( $collection, $subsetCollection, SubsetFinderConfig::forPerformance() ); // For balanced approach (256MB memory, lazy evaluation enabled) $subsetFinder = new SubsetFinder( $collection, $subsetCollection, SubsetFinderConfig::forBalanced() );
Using the Facade
use Ozdemir\SubsetFinder\Facades\SubsetFinder; // Create with default configuration $subsetFinder = SubsetFinder::create($collection, $subsetCollection); // Create with specific profile $subsetFinder = SubsetFinder::forLargeDatasets($collection, $subsetCollection); $subsetFinder = SubsetFinder::forPerformance($collection, $subsetCollection);
Using the Trait
use Ozdemir\SubsetFinder\Traits\HasSubsetOperations; class ProductCollection extends Collection { use HasSubsetOperations; } $products = new ProductCollection([...]); // Find subsets directly on the collection $subsetFinder = $products->findSubsets($subsetCollection); // Use profiles $subsetFinder = $products->findSubsetsWithProfile($subsetCollection, 'large_datasets'); // Check feasibility if ($products->canSatisfySubsets($subsetCollection)) { // Proceed with subset creation }
Use Cases
E-commerce Bundle Creation
$products = collect([ new Product(id: 1, quantity: 100, price: 10), // T-shirt new Product(id: 2, quantity: 50, price: 5), // Socks new Product(id: 3, quantity: 25, price: 20), // Hat ]); $bundles = new SubsetCollection([ Subset::of([1, 2])->take(2), // T-shirt + Socks bundle Subset::of([1, 3])->take(1), // T-shirt + Hat bundle ]); $subsetFinder = new SubsetFinder($products, $bundles); $subsetFinder->solve(); // Create 25 T-shirt + Socks bundles // Create 25 T-shirt + Hat bundles // Remaining: 25 T-shirts, 0 socks, 0 hats
Inventory Management
$inventory = collect([ new Item(id: 'A', quantity: 100, category: 'electronics'), new Item(id: 'B', quantity: 200, category: 'clothing'), new Item(id: 'C', quantity: 150, category: 'books'), ]); $orders = new SubsetCollection([ Subset::of(['A', 'B'])->take(10), // Electronics + Clothing order Subset::of(['C'])->take(5), // Books order ]); $subsetFinder = new SubsetFinder($inventory, $orders); $subsetFinder->solve();
Configuration
Environment Variables
SUBSET_FINDER_MAX_MEMORY=256M SUBSET_FINDER_LAZY_EVALUATION=true SUBSET_FINDER_LOGGING=true SUBSET_FINDER_LOG_CHANNEL=subset-finder SUBSET_FINDER_LOG_LEVEL=info
Configuration File
// config/subset-finder.php return [ 'defaults' => [ 'id_field' => 'id', 'quantity_field' => 'quantity', 'sort_field' => 'id', 'sort_descending' => false, 'max_memory_usage' => env('SUBSET_FINDER_MAX_MEMORY', 128 * 1024 * 1024), 'enable_lazy_evaluation' => env('SUBSET_FINDER_LAZY_EVALUATION', true), 'enable_logging' => env('SUBSET_FINDER_LOGGING', false), ], 'profiles' => [ 'large_datasets' => [ 'max_memory_usage' => 512 * 1024 * 1024, 'enable_lazy_evaluation' => true, 'enable_logging' => true, ], 'performance' => [ 'max_memory_usage' => 64 * 1024 * 1024, 'enable_lazy_evaluation' => false, 'enable_logging' => false, ], ], ];
Performance Monitoring
$subsetFinder = new SubsetFinder($collection, $subsetCollection); $subsetFinder->solve(); // Get performance metrics $metrics = $subsetFinder->getPerformanceMetrics(); // [ // 'execution_time_ms' => 45.23, // 'memory_peak_mb' => 12.5, // 'memory_increase_mb' => 8.2, // 'collection_size' => 1000, // 'subset_count' => 5, // 'found_subsets_count' => 5, // 'remaining_items_count' => 50 // ] // Check solution quality $isOptimal = $subsetFinder->isOptimal(); // true if no remaining items $efficiency = $subsetFinder->getEfficiencyPercentage(); // 95.2%
Error Handling
use Ozdemir\SubsetFinder\Exceptions\InvalidArgumentException; use Ozdemir\SubsetFinder\Exceptions\InsufficientQuantityException; try { $subsetFinder = new SubsetFinder($collection, $subsetCollection); $subsetFinder->solve(); } catch (InvalidArgumentException $e) { // Handle invalid input (empty collection, invalid items, etc.) Log::error('Invalid subset finder input: ' . $e->getMessage()); } catch (InsufficientQuantityException $e) { // Handle insufficient quantities Log::warning('Cannot create subsets: ' . $e->getMessage()); }
Testing
# Run tests composer test # Run tests with coverage composer test-coverage # Run static analysis composer analyse
Performance Tips
- Use appropriate configuration profiles for your dataset size
- Enable lazy evaluation for large collections to reduce memory usage
- Monitor memory usage and adjust
max_memory_usage
accordingly - Use meaningful sort fields to optimize subset selection
- Consider batch processing for very large datasets
Advanced Usage
Custom Logging
use Psr\Log\LoggerInterface; class CustomLogger implements LoggerInterface { // Implement logger methods } $subsetFinder = new SubsetFinder( $collection, $subsetCollection, $config, new CustomLogger() );
Memory Management
// Check memory before processing if (memory_get_usage(true) > $config->maxMemoryUsage) { throw new \Exception('Insufficient memory for processing'); } // Process in batches for very large datasets $batchSize = 1000; foreach ($collection->chunk($batchSize) as $batch) { // Process batch }
Contributing
Contributions are welcome! Please see our Contributing Guide for details.
License
This package is open-sourced software licensed under the MIT License.
Support
- Documentation: GitHub Wiki
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Roadmap
✅ Redis Caching Support
- High-performance caching with Redis integration
- Memory-based fallback when Redis is unavailable
- Smart cache key generation based on input data
- Configurable TTL and cache management
- Automatic fallback to memory cache on Redis failure
✅ Parallel Processing
- Multi-process subset finding for large datasets
- Intelligent chunking with optimal size calculation
- System-aware process limits based on CPU and memory
- Simulated parallel processing for development environments
- Performance metrics for parallel operations
✅ Weighted Subset Selection
- Multi-criteria optimization with configurable weights
- Advanced constraint handling (ranges, custom functions)
- Efficiency scoring and ranking algorithms
- Statistical analysis with quartiles and distributions
- Real-world optimization scenarios (e-commerce, inventory)
🔄 In Development
- Machine learning-based optimization
- GraphQL integration
- Performance benchmarking tools
- More configuration profiles