jeffersongoncalves/laravel-hunter
Laravel Hunter by Jefferson Goncalves is a lightweight diagnostic package for Laravel applications. It helps you hunt down hidden issues, inspect problem areas, and keep your codebase clean with developer-friendly tooling built to speed up debugging and maintenance.
This is the initial 1.0.0 release, so treat the public API as stable-but-young: pin ^1.0 in composer.json and read the changelog on every minor bump until the package proves itself over a few releases.
Install and wire up (5 minutes):
composer require <vendor>/laravel-hunter # exact vendor name from Packagist
php artisan vendor:publish --tag="hunter-config"
Add the key to .env (never hard-code it):
HUNTER_API_KEY=your_key_here
Then clear config cache: php artisan config:clear.
Where to look first, in order:
config/hunter.php — base URL, timeout, retry settings, and the api_key binding.Hunter::) and whether an interface is bound for swapping.Collections, or typed objects back. This determines how much defensive coding you need downstream.First real use case: verifying an email at the point of capture. In a form request or registration listener:
use Hunter; // or the injected contract
$result = Hunter::emailVerifier($request->email);
if ($result->status === 'invalid') {
throw ValidationException::withMessages([
'email' => 'That email address does not appear to be deliverable.',
]);
}
This is the highest-value, lowest-risk integration: one call, one decision, clearly bounded. Do this before you touch leads or campaigns — the campaign endpoints have far more surface area and cost.
Prefer constructor injection over the facade. Even if the package ships a facade, resolve the underlying client through the container so you can swap it in tests:
public function __construct(private \Vendor\Hunter\HunterClient $hunter) {}
If the package binds an interface, bind it to a fake in a test service provider instead of hitting Http::fake() everywhere.
Wrap the client in a thin application-level service. Do not let Hunter's response shape leak into your controllers. Three reasons: (a) you may switch providers later, (b) you want one place to normalise null vs not_found vs exception, (c) you want one place to attach logging and credit accounting.
class EmailEnrichment
{
public function __construct(private HunterClient $hunter) {}
public function verify(string $email): VerificationOutcome
{
$response = $this->hunter->emailVerifier($email);
return new VerificationOutcome(
status: $response->status ?? 'unknown',
score: $response->score ?? 0,
raw: $response->toArray(),
);
}
}
Cache aggressively — this is a paid, credit-metered API. Account info barely changes; verifier results are stable for weeks. A 24-hour cache on account info and a 7–30 day cache on verifier results will pay for itself immediately:
return Cache::remember("hunter:verify:{$email}", now()->addDays(14), fn () => ...);
Bulk work belongs in queued jobs with an explicit rate limiter. Domain search and email finder are the calls you'll want to fan out over a prospect list. Each job should be individually retryable and individually rate-limited:
// AppServiceProvider::boot()
RateLimiter::for('hunter', fn () => Limit::perMinute(10));
// job
public function handle(): void
{
$result = $this->hunter->emailFinder($this->domain, $this->first, $this->last);
...
}
Set $tries = 3, $backoff = [10, 60, 300], and make the job idempotent — a retried email finder that already succeeded server-side will still burn a credit on the retry unless you short-circuit on cached/persisted results. Check your own database first.
Pagination on leads and campaigns. These listings are windowed — Hunter caps how far back you can page. Build your sync to checkpoint on a stored cursor or created_at watermark rather than blindly incrementing offset, otherwise a full re-sync will silently miss records once you exceed the window.
Integration with Eloquent: store a normalised record per contact (hunter_status, hunter_score, hunter_checked_at) rather than re-querying. A small HasHunterVerification trait with a refreshVerification() method keeps it tidy and makes the API call site obvious in code review.
Feature-test everything. Never let a test suite reach the real API — one CI run over a fixture set can drain a month of credits. If the package ships a fake, use it; if not, Http::fake() on the base URL and assert on the outgoing request body.
404/not found from email finder can still count against your plan. Never fire speculative calls in a loop, in a page load, or in an observer that runs on every model save.null in the data payload. Code that only branches on exceptions will crash on $result->emails[0]. Always null-check the inner payload before indexing.unknown frequently. Disposable domains, catch-all servers, and greylisting all produce unknown with a low score. Decide your policy up front — usually: valid → accept, invalid → reject, unknown → accept but flag for re-check later. Treating unknown as invalid will reject real customers.env() outside config/ is a trap. If you find yourself writing env('HUNTER_API_KEY') in a service class, stop — it returns null the moment anyone runs php artisan config:cache. Always go through config('hunter.api_key').$this->release(60)) rather than letting the HTTP layer hammer it.?? defaults in your adapter layer.account() is the cheapest way to confirm what the key can actually do — call it once on deploy and log the result.api_key from any Http::beforeSending logging.PendingRequest or a Guzzle instance), which lets you add a custom User-Agent, a trace header, or a middleware that logs request IDs. If the client is not injectable, you're limited to the config file — note that as a limitation when you evaluate the 1.1 release.config/hunter.php in your app will not be updated by composer update — you'll need to re-publish and diff it manually.How can I help you explore Laravel packages today?