Skip to content

Hunter Examples

This document contains practical examples of using Hunter for different scenarios and use cases.

Table of Contents

  1. Basic Operations
  2. Conditional Processing
  3. Callbacks and Hooks
  4. Dry Run and Testing
  5. Progress Tracking
  6. Convenience Methods
  7. Flow Control
  8. Advanced Use Cases
  9. Performance Optimization

Basic Operations

Simple Update

php
use Hunter\Hunter;
use App\Models\User;

// Activate all inactive users using the class method
$result = Hunter::for(User::class)
    ->find('status', 'inactive')
    ->then(function ($user) {
        $user->update(['status' => 'active']);
    })
    ->hunt();

echo "Users activated: {$result->successful}";

// Or use the convenient helper function
$result = hunter(User::class)
    ->find('status', 'inactive')
    ->then(function ($user) {
        $user->update(['status' => 'active']);
    })
    ->hunt();

### Search with Operators

```php
// Process users created in the last 30 days
$result = Hunter::for(User::class)
    ->find('created_at', '>', now()->subDays(30))
    ->then(function ($user) {
        $user->update(['welcome_email_sent' => true]);
    })
    ->hunt();

Custom Query Building

php
$result = Hunter::for(User::class)
    ->find('status', 'active')
    ->modifyQueryUsing(function ($query) {
        return $query->whereHas('posts', function ($q) {
            $q->where('published', true);
        })->where('last_login', '<', now()->subMonths(6));
    })
    ->then(function ($user) {
        $user->update(['status' => 'inactive']);
    })
    ->hunt();

Conditional Processing

Using thenIf with Closures

php
$result = Hunter::for(User::class)
    ->find('status', 'pending')
    ->thenIf(
        fn($user) => $user->posts()->count() === 0,
        fn($user) => $user->delete()
    )
    ->thenIf(
        fn($user) => $user->last_login < now()->subYear(),
        fn($user) => $user->update(['status' => 'dormant'])
    )
    ->hunt();

Simple Boolean Conditions

php
$sendNotifications = true;

$result = Hunter::for(User::class)
    ->find('notification_pending', true)
    ->thenIf($sendNotifications, function ($user) {
        $user->notify(new WeeklyDigest());
        $user->update(['notification_pending' => false]);
    })
    ->hunt();

Conditions with Context

php
$result = Hunter::for(Order::class)
    ->find('status', 'pending')
    ->thenIf(
        fn($order, $hunter) => $order->created_at < now()->subHours(24),
        fn($order) => $order->update(['status' => 'expired']),
        ['timeout_hours' => 24]
    )
    ->hunt();

Callbacks and Hooks

Complete Lifecycle

php
$result = Hunter::for(User::class)
    ->find('status', 'pending_verification')
    ->onBeforeThen(function ($user, $hunter) {
        // Validation before processing
        if ($user->email === null) {
            $hunter->skip($user, 'Missing email address');
            return;
        }

        Log::info("Starting verification for user {$user->id}");
    })
    ->then(function ($user) {
        // Main processing
        $user->sendEmailVerification();
        $user->update(['verification_sent_at' => now()]);
    })
    ->onAfterThen(function ($user) {
        // Actions after processing
        Log::info("Verification sent to user {$user->id}");
    })
    ->onSuccess(function ($user) {
        // Success handling
        $user->increment('verification_attempts');
    })
    ->onError(function ($user, $exception, $hunter) {
        // Error handling
        Log::error("Failed to send verification to user {$user->id}: {$exception->getMessage()}");
        $user->update(['verification_error' => $exception->getMessage()]);
    })
    ->hunt();

Dry Run and Testing

Testing Dangerous Operations

php
// First, test with dry run
$dryResult = Hunter::for(User::class)
    ->find('last_login', '<', now()->subYear())
    ->whereNull('premium_expires_at')
    ->dryRun() // Important!
    ->then(function ($user) {
        $user->delete(); // Won't be executed
    })
    ->hunt();

echo "Users that would be deleted: {$dryResult->successful}\n";

// If everything looks good, run without dry run
if (confirm("Delete {$dryResult->successful} users?")) {
    $realResult = Hunter::for(User::class)
        ->find('last_login', '<', now()->subYear())
        ->whereNull('premium_expires_at')
        ->then(function ($user) {
            $user->delete();
        })
        ->hunt();

    echo "Users deleted: {$realResult->successful}\n";
}

Dry Run with Detailed Logging

php
$result = Hunter::for(Product::class)
    ->find('stock', 0)
    ->dryRun()
    ->withLogging('product-cleanup')
    ->then(function ($product, $hunter) {
        Log::info("Would archive product: {$product->name}");
        $product->update(['archived' => true]);
    })
    ->hunt();

echo "Products that would be archived: {$result->successful}\n";

Progress Tracking

Simple Progress Reporting

php
$result = Hunter::for(User::class)
    ->find('status', 'pending')
    ->onProgress(function ($processed, $total, $percentage) {
        echo "Progress: {$percentage}% ({$processed}/{$total})\n";
    })
    ->then(function ($user) {
        // Process user
        sleep(1); // Simulate slow processing
        $user->update(['processed' => true]);
    })
    ->hunt();

Advanced Progress with Statistics

php
$result = Hunter::for(Order::class)
    ->find('status', 'pending')
    ->onProgress(function ($processed, $total, $percentage, $successful, $failed, $skipped, $result) {
        if ($processed % 100 === 0 || $percentage >= 100) {
            echo sprintf(
                "Progress: %d%% | Processed: %d/%d | Success: %d | Failed: %d | Skipped: %d\n",
                $percentage, $processed, $total, $successful, $failed, $skipped
            );
        }
    })
    ->then(function ($order, $hunter) {
        try {
            $order->processPayment();
            $order->update(['status' => 'completed']);
        } catch (PaymentException $e) {
            $hunter->fail($order, "Payment failed: {$e->getMessage()}");
        }
    })
    ->hunt();

Custom Progress Tracker

php
class ProgressTracker
{
    private $startTime;
    private $lastUpdate;

    public function __construct()
    {
        $this->startTime = time();
        $this->lastUpdate = time();
    }

    public function track($processed, $total, $percentage, $successful, $failed, $skipped)
    {
        $now = time();
        $elapsed = $now - $this->startTime;
        $rate = $processed > 0 ? $processed / $elapsed : 0;
        $eta = $rate > 0 ? ($total - $processed) / $rate : 0;

        if ($now - $this->lastUpdate >= 5) { // Update every 5 seconds
            echo sprintf(
                "[%s] %d%% | %d/%d | Rate: %.1f/s | ETA: %s | S:%d F:%d Sk:%d\n",
                date('H:i:s'),
                $percentage,
                $processed,
                $total,
                $rate,
                $this->formatTime($eta),
                $successful,
                $failed,
                $skipped
            );
            $this->lastUpdate = $now;
        }
    }

    private function formatTime($seconds)
    {
        if ($seconds < 60) return sprintf('%ds', $seconds);
        if ($seconds < 3600) return sprintf('%dm%ds', $seconds / 60, $seconds % 60);
        return sprintf('%dh%dm', $seconds / 3600, ($seconds % 3600) / 60);
    }
}

$tracker = new ProgressTracker();

$result = Hunter::for(User::class)
    ->find('status', 'pending')
    ->onProgress([$tracker, 'track'])
    ->then(function ($user) {
        // Process user
    })
    ->hunt();

Convenience Methods

Advanced Filtering

php
$result = Hunter::for(User::class)
    ->whereIn('role', ['admin', 'editor', 'moderator'])
    ->whereNotNull('email_verified_at')
    ->whereBetween('created_at', [now()->subYear(), now()->subMonth()])
    ->whereNull('deleted_at')
    ->latest('last_login')
    ->limit(1000)
    ->with(['profile', 'permissions'])
    ->then(function ($user) {
        $user->update(['last_processed' => now()]);
    })
    ->hunt();

Ordering and Limiting

php
// Process the 100 oldest users without login
$result = Hunter::for(User::class)
    ->whereNull('last_login')
    ->oldest('created_at')
    ->limit(100)
    ->then(function ($user) {
        $user->sendReengagementEmail();
    })
    ->hunt();

// Process newest draft posts
$result = Hunter::for(Post::class)
    ->find('status', 'draft')
    ->latest('updated_at')
    ->with(['author', 'categories'])
    ->then(function ($post) {
        if ($post->shouldAutoPublish()) {
            $post->publish();
        }
    })
    ->hunt();

Optimized Eager Loading

php
$result = Hunter::for(User::class)
    ->find('status', 'active')
    ->with([
        'posts' => function ($query) {
            $query->where('published', true)->latest();
        },
        'profile:user_id,avatar,bio',
        'subscriptions.plan'
    ])
    ->then(function ($user) {
        // User already has posts, profile and subscriptions loaded
        $user->calculateEngagementScore();
    })
    ->hunt();

Flow Control

Conditional Skipping

php
$result = Hunter::for(User::class)
    ->find('status', 'pending_deletion')
    ->then(function ($user, $hunter) {
        // Pre-deletion checks
        if ($user->orders()->where('status', 'pending')->exists()) {
            $hunter->skip($user, 'Has pending orders');
            return;
        }

        if ($user->balance > 0) {
            $hunter->skip($user, 'Has positive balance');
            return;
        }

        if ($user->created_at > now()->subDays(30)) {
            $hunter->skip($user, 'Account too recent');
            return;
        }

        // Safe to delete
        $user->anonymize();
        $user->delete();
    })
    ->hunt();

// Analyze results
foreach ($result->skipReasons as $userId => $reason) {
    echo "User {$userId} skipped: {$reason}\n";
}

Stopping on Critical Conditions

php
$result = Hunter::for(Order::class)
    ->find('status', 'processing')
    ->then(function ($order, $hunter) {
        // Check API rate limit
        if (ExternalApi::getRemainingCalls() < 10) {
            $hunter->stop('API rate limit approaching');
            return;
        }

        try {
            $order->processPayment();
        } catch (CriticalSystemException $e) {
            $hunter->stop("Critical system error: {$e->getMessage()}");
            return;
        } catch (PaymentException $e) {
            $hunter->fail($order, "Payment failed: {$e->getMessage()}");
            return;
        }

        $order->update(['status' => 'completed']);
    })
    ->hunt();

if ($result->wasStopped()) {
    echo "Processing stopped: {$result->getStopReason()}\n";
    echo "Processed {$result->successful} orders before stopping\n";
}

Fail with Retry Logic

php
$result = Hunter::for(EmailQueue::class)
    ->find('status', 'pending')
    ->whereNull('processing_at')
    ->then(function ($email, $hunter) {
        try {
            $email->send();
            $email->update(['status' => 'sent', 'sent_at' => now()]);

        } catch (TemporaryFailureException $e) {
            // Temporary failure - schedule retry
            $email->increment('retry_count');

            if ($email->retry_count >= 3) {
                $hunter->fail($email, "Max retries exceeded: {$e->getMessage()}");
            } else {
                $hunter->skip($email, "Temporary failure, will retry: {$e->getMessage()}");
                $email->update(['processing_at' => now()->addMinutes(5 * $email->retry_count)]);
            }

        } catch (PermanentFailureException $e) {
            $hunter->fail($email, "Permanent failure: {$e->getMessage()}");
            $email->update(['status' => 'failed']);
        }
    })
    ->hunt();

Advanced Use Cases

Complex Data Migration

php
$result = Hunter::for(LegacyUser::class)
    ->find('migrated', false)
    ->chunk(50) // Smaller chunks for complex operations
    ->onProgress(function ($processed, $total, $percentage) {
        echo "Migrating users: {$percentage}% ({$processed}/{$total})\n";
    })
    ->then(function ($legacyUser, $hunter) {
        DB::transaction(function () use ($legacyUser, $hunter) {
            try {
                // Create new user
                $newUser = User::create([
                    'name' => $legacyUser->full_name,
                    'email' => $legacyUser->email_address,
                    'created_at' => $legacyUser->registration_date,
                    'legacy_id' => $legacyUser->id,
                ]);

                // Migrate posts
                foreach ($legacyUser->articles as $article) {
                    Post::create([
                        'user_id' => $newUser->id,
                        'title' => $article->headline,
                        'content' => $article->body,
                        'published_at' => $article->publish_date,
                    ]);
                }

                // Mark as migrated
                $legacyUser->update(['migrated' => true, 'new_user_id' => $newUser->id]);

            } catch (Exception $e) {
                $hunter->fail($legacyUser, "Migration failed: {$e->getMessage()}");
            }
        });
    })
    ->hunt();

echo "Migration completed:\n";
echo "- Success: {$result->successful}\n";
echo "- Failed: {$result->failed}\n";
echo "- Time: {$result->getExecutionTime()}s\n";
echo "- Memory: {$result->getFormattedMemoryUsage()}\n";

File Processing

php
$result = Hunter::for(Document::class)
    ->find('status', 'pending_processing')
    ->whereNotNull('file_path')
    ->chunk(10) // Process few files at a time
    ->onProgress(function ($processed, $total, $percentage) {
        echo "Processing files: {$percentage}%\n";
    })
    ->then(function ($document, $hunter) {
        try {
            if (!Storage::exists($document->file_path)) {
                $hunter->fail($document, 'File not found');
                return;
            }

            $fileSize = Storage::size($document->file_path);
            if ($fileSize > 100 * 1024 * 1024) { // 100MB
                $hunter->skip($document, 'File too large');
                return;
            }

            // Process file
            $processor = new DocumentProcessor();
            $result = $processor->process($document->file_path);

            $document->update([
                'status' => 'processed',
                'processed_at' => now(),
                'processing_result' => $result,
            ]);

        } catch (ProcessingException $e) {
            $hunter->fail($document, "Processing error: {$e->getMessage()}");
        }
    })
    ->hunt();

Automated Cleanup with Checks

php
$result = Hunter::for(TempFile::class)
    ->find('created_at', '<', now()->subDays(7))
    ->onProgress(function ($processed, $total, $percentage) {
        if ($processed % 50 === 0) {
            echo "Cleaning temp files: {$percentage}%\n";
        }
    })
    ->then(function ($tempFile, $hunter) {
        try {
            // Check if file still exists
            if (!Storage::exists($tempFile->path)) {
                $tempFile->delete();
                return;
            }

            // Check if file is being used
            if ($tempFile->isBeingUsed()) {
                $hunter->skip($tempFile, 'File is currently being used');
                return;
            }

            // Delete file and record
            Storage::delete($tempFile->path);
            $tempFile->delete();

        } catch (Exception $e) {
            $hunter->fail($tempFile, "Cleanup failed: {$e->getMessage()}");
        }
    })
    ->hunt();

echo "Cleanup completed:\n";
echo "- Files removed: {$result->successful}\n";
echo "- Failed: {$result->failed}\n";
echo "- Skipped: {$result->skipped}\n";
echo "- Execution time: {$result->getExecutionTime()}s\n";
echo "- Memory usage: {$result->getFormattedMemoryUsage()}\n";

// Display results in a beautiful console table
$result->consoleTable();

Result Reporting and Display

Console Table Output

Hunter provides a beautiful console table display using Laravel Prompts for better result visualization:

php
$result = Hunter::for(User::class)
    ->find('status', 'pending')
    ->onProgress(function ($processed, $total, $percentage) {
        echo "Processing: {$percentage}%\n";
    })
    ->then(function ($user) {
        $user->update(['processed' => true]);
    })
    ->hunt();

// Display results in a formatted table
$result->consoleTable();

/*
Output:
┌─────────────────┬─────────┬──────────┐
│ Metric          │ Value   │ Details  │
├─────────────────┼─────────┼──────────┤
│ Total Records   │ 1,250   │          │
│ Successful      │ 1,200   │ 96.0%    │
│ Failed          │ 25      │ 2.0%     │
│ Skipped         │ 25      │ 2.0%     │
│ Execution Time  │ 45.67s  │          │
│ Memory Usage    │ 32.5 MB │          │
│ Status          │ Completed│         │
│ Processing Rate │ 27.4 records/sec │ │
└─────────────────┴─────────┴──────────┘

⚠️  25 record(s) failed to process. Check logs for details.
ℹ️  25 record(s) were skipped.

Skip reasons:
  • Missing email address: 15 record(s)
  • Account locked: 10 record(s)
*/

Comparing Results

php
// Compare dry run vs actual execution
$dryResult = Hunter::for(User::class)
    ->find('status', 'inactive')
    ->dryRun()
    ->then(fn($user) => $user->delete())
    ->hunt();

echo "=== DRY RUN RESULTS ===\n";
$dryResult->consoleTable();

if (confirm("Proceed with actual execution?")) {
    $realResult = Hunter::for(User::class)
        ->find('status', 'inactive')
        ->then(fn($user) => $user->delete())
        ->hunt();

    echo "\n=== ACTUAL EXECUTION RESULTS ===\n";
    $realResult->consoleTable();
}

Laravel Command Integration with Console Output

php
class ProcessUsersCommand extends Command
{
    public function handle(): int
    {
        $result = Hunter::for(User::class)
            ->find('status', 'pending')
            ->onProgress(function ($processed, $total, $percentage) {
                $this->line("Processing: {$percentage}% ({$processed}/{$total})");
            })
            ->then(function ($user) {
                $user->update(['processed' => true]);
            })
            ->hunt();

        $this->newLine();
        $this->info('Processing completed! Results:');
        $result->consoleTable();

        return $result->hasErrors() ? self::FAILURE : self::SUCCESS;
    }
}

External API Synchronization

php
$result = Hunter::for(Product::class)
    ->find('sync_status', 'pending')
    ->orderBy('priority', 'desc')
    ->chunk(25)
    ->onProgress(function ($processed, $total, $percentage) {
        echo "Syncing products: {$percentage}%\n";
    })
    ->then(function ($product, $hunter) {
        try {
            $api = new ExternalProductApi();

            // Check rate limit
            if (!$api->canMakeRequest()) {
                $hunter->stop('API rate limit reached');
                return;
            }

            $externalData = $api->getProduct($product->external_id);

            if (!$externalData) {
                $hunter->fail($product, 'Product not found in external API');
                return;
            }

            // Update product
            $product->update([
                'name' => $externalData['name'],
                'price' => $externalData['price'],
                'stock' => $externalData['stock'],
                'sync_status' => 'synced',
                'last_sync' => now(),
            ]);

        } catch (ApiException $e) {
            if ($e->isRetryable()) {
                $hunter->skip($product, "Temporary API error: {$e->getMessage()}");
            } else {
                $hunter->fail($product, "API error: {$e->getMessage()}");
            }
        }
    })
    ->hunt();

Performance Optimization

Query Optimization

php
// ❌ Inefficient
$result = Hunter::for(User::class)
    ->find('status', 'active')
    ->then(function ($user) {
        $user->posts()->where('published', true)->count(); // N+1 query
    })
    ->hunt();

// ✅ Efficient
$result = Hunter::for(User::class)
    ->find('status', 'active')
    ->with(['posts' => fn($q) => $q->where('published', true)])
    ->then(function ($user) {
        $user->posts->count(); // No additional query
    })
    ->hunt();

Appropriate Chunk Sizes

php
// For simple operations
Hunter::for(User::class)->chunk(1000)

// For operations with multiple queries
Hunter::for(User::class)->chunk(100)

// For heavy operations (API calls, file processing)
Hunter::for(Document::class)->chunk(10)

Memory Monitoring

php
$result = Hunter::for(User::class)
    ->find('status', 'pending')
    ->onProgress(function ($processed, $total, $percentage, $successful, $failed, $skipped, $result, $hunter) {
        $currentMemory = memory_get_usage(true);
        $peakMemory = memory_get_peak_usage(true);

        echo sprintf(
            "Progress: %d%% | Memory: %s | Peak: %s\n",
            $percentage,
            $this->formatBytes($currentMemory),
            $this->formatBytes($peakMemory)
        );

        // Force garbage collection if needed
        if ($currentMemory > 100 * 1024 * 1024) { // 100MB
            gc_collect_cycles();
        }
    })
    ->then(function ($user) {
        // Process user
    })
    ->hunt();

Best Practices

1. Use Appropriate Chunk Sizes

  • Simple operations (basic updates): 1000-2000 records
  • Medium operations (multiple queries): 100-500 records
  • Heavy operations (file processing, API calls): 10-50 records

2. Always Test with Dry Run First

php
// Test first
$dryResult = Hunter::for(User::class)
    ->find('status', 'inactive')
    ->dryRun()
    ->then(fn($user) => $user->delete())
    ->hunt();

echo "Would delete: {$dryResult->successful} records\n";

3. Monitor Progress for Long Operations

php
$hunter->onProgress(function ($processed, $total, $percentage) {
    if ($processed % 100 === 0) {
        echo "Progress: {$percentage}%\n";
    }
});

4. Handle Errors Gracefully

php
$hunter->then(function ($record, $hunter) {
    try {
        // Risky operation
        $record->complexOperation();
    } catch (RetryableException $e) {
        $hunter->skip($record, "Will retry: {$e->getMessage()}");
    } catch (FatalException $e) {
        $hunter->fail($record, "Fatal: {$e->getMessage()}");
    }
});
php
$hunter->with(['posts', 'profile'])
    ->then(function ($user) {
        // $user->posts and $user->profile are already loaded
    });