lukeraymonddowning/honey
A simple Laravel honeypot package that protects forms from spam bots without CAPTCHAs. Add a hidden field, and Honey silently rejects submissions that fill it, keeping forms user-friendly and spam-free.
Install the package and wire it up in three commands:
composer require lukeraymonddowning/honey
php artisan vendor:publish --tag=honey-config
The published config/honey.php is where every knob lives: honeypot, reCAPTCHA, IP blocking, and the minimum-time-on-page rule. Read this file first — it tells you what the package will and won't do before you touch a form.
Add protection to a form with the Blade directive:
<form method="POST" action="/contact">
@honey
@csrf
<input name="email">
<button type="submit">Send</button>
</form>
Then guard the controller. Either apply the middleware (->middleware('honey')) for blanket protection, or call the facade yourself:
use Lukeraymonddowning\Honey\Facades\Honey;
public function store(Request $request)
{
if (Honey::check()->isSpam()) {
abort(403, 'Spam detected.');
}
// ...
}
First use case: a public contact form or newsletter signup. That's where honeypot + reCAPTCHA v3 pays for itself with almost no effort.
Middleware for boilerplate, facade for fine control. Slap the honey middleware on every public POST route and forget about it. Drop to Honey::check() when you need to redirect, log, score, or show a specific message instead of a blunt abort.
Response object is your instrumentation point. Honey::check() returns a response with isSpam(), hasBeenBlocked(), isBlocked(), and failedFor(). Use failedFor() to log which technique caught the request (honeypot, recaptcha, min-time) — that's how you tune thresholds over time.
FormRequest integration. Put the check inside a FormRequest's authorize() or a withValidator() callback so validation and spam rejection are colocated with the rules for that endpoint.
reCAPTCHA v3 is score-based. Configure recaptcha.site_key, secret_key, and minimum_score in the config. The @honey directive renders the script tag that generates the token. For long forms (surveys, multi-step wizards), regenerate the token on submit — tokens expire after roughly two minutes.
Livewire needs the trait, not the middleware. Livewire submits over AJAX after the initial page load, so route middleware only fires on the first request. Use Honey::check() inside the component's submit handler, or the package's Livewire trait if you're on a version that ships one.
Layer with rate limiting. Honey catches bots, throttle catches abuse. Route::post('/contact', ...)->middleware(['honey', 'throttle:10,1']) is a sane default.
Persist IP blocks. The default in-memory manager evaporates on every deploy and every queue worker restart. For anything beyond a hobby app, swap in a cache- or database-backed IpManager so blocks actually stick.
Testing. Disable honeypot/recaptcha in phpunit.xml env (HONEY_HONEYPOT_ENABLED=false, HONEY_RECAPTCHA_ENABLED=false) for the happy-path suite, then write separate tests that turn them back on to assert spam rejection.
Proxies break IP blocking. Behind Cloudflare, an ALB, or any reverse proxy, $request->ip() returns the edge node, not the client. Configure TrustProxies with the right HEADER_X_FORWARDED_FOR or your blocklist will quietly ban everyone — including legitimate users.
Minimum time on page false-positives. The min_time_on_page threshold is measured from page render. Password managers, browser back/forward cache, and very fast form fillers can trip it. Keep it to 1–2 seconds; anything higher punishes legitimate users more than bots.
Don't name the honeypot field in your JS. The field name is randomized per config/session — hardcoding it in frontend code defeats the point and will break on config changes. Let @honey render it.
reCAPTCHA v3 needs the script loaded before submit. If you conditionally include @honey, grecaptcha may be undefined when the token is requested. Include it unconditionally on any page that submits to a protected endpoint.
Randomize the honeypot, but know where it's stored. The randomization prevents bots from learning one field name across all sites, but it means you can't grep your codebase for it later — rely on the directive, not manual hidden inputs.
Order of checks matters for diagnostics. If honeypot fires first, reCAPTCHA never runs and you lose signal on whether the token was even valid. If you're debugging false positives, disable honeypot temporarily to isolate.
IP blocking has no allowlist by default. Add an early return for admin/internal IPs before the middleware or in a custom manager — otherwise a bot behind shared NAT can lock out an entire office.
Extension points. The IpManager contract is the cleanest override — implement a cache-backed or Redis-backed version for multi-server deployments. The reCAPTCHA client can also be swapped if you need to mock it in tests or point at a different verification endpoint.
Config caching. After php artisan config:cache, changing .env values for reCAPTCHA keys won't take effect until you recache. This burns people in staging constantly.
Fail open vs. fail closed. If the reCAPTCHA verification endpoint times out, the package's behavior depends on config. Decide deliberately: failing closed rejects real users during a Google outage; failing open lets spam through. For low-stakes forms, fail open.
How can I help you explore Laravel packages today?