nickurt/laravel-stopforumspam
Laravel package for StopForumSpam.com that checks usernames, email addresses and IPs against the spam database. Ships validation rules, form requests, caching and configurable API endpoints to block spam registrations and forum posts.
Install and let auto-discovery do the work (Laravel 11/12/13):
composer require nickurt/laravel-stopforumspam
php artisan vendor:publish --provider="Nickurt\StopForumSpam\ServiceProvider" --tag="config"
Set the environment switches in .env:
STOPFORUMSPAM_ENABLED=true
STOPFORUMSPAM_IP=true
STOPFORUMSPAM_USERNAME=true
STOPFORUMSPAM_EMAIL=true
Where to look first: config/stopforumspam.php (the per-channel toggles and cache TTLs), then src/StopForumSpam.php and src/Rules/ in the vendor directory. Read those two before the README — the package is small and the README skims the response shape, which is what you actually need for threshold decisions.
First use case: screen a registration request. Either through the facade:
use Nickurt\StopForumSpam\Facades\StopForumSpam;
if (StopForumSpam::setIp($request->ip())->isSpam()) {
// reject or flag
}
or as a validation rule on the relevant fields. The rule under Nickurt\StopForumSpam\Rules\StopForumSpam detects whether the value is an IP, an email, or a username and queries the matching StopForumSpam endpoint. If your installed version also ships StopForumSpamEmail / StopForumSpamIp / StopForumSpamUsername, prefer those — explicit rules make the intent obvious in a FormRequest and are easier to unit test.
FormRequest validation (the common path). Use the rule alongside normal format validation, and put it last so cheap checks fail first:
public function rules(): array
{
return [
'email' => ['required', 'email:rfc,dns', new StopForumSpam()],
'username' => ['required', 'string', 'min:3', new StopForumSpam()],
];
}
Wrap the attribute in bail if you don't want a second network call after a format failure.
Combined lookup instead of three round trips. The API accepts multiple parameters in one request. Prefer:
$response = StopForumSpam::setIp($request->ip())
->setEmail($request->email)
->setUsername($request->username)
->getResponse();
If your version's isSpam() short-circuits per setter, you'll pay three HTTP calls per registration — measure before shipping. This is the single biggest performance decision in the integration.
Threshold on the raw response, not the boolean. isSpam() collapses everything to appears == 1. In practice you want graduated handling:
$data = StopForumSpam::setEmail($email)->getResponse();
$confidence = (int) ($data['email']['confidence'] ?? 0);
$frequency = (int) ($data['email']['frequency'] ?? 0);
if ($confidence >= 50 || $frequency >= 5) {
// hard block / manual review
} elseif ($confidence > 0) {
// allow, but flag for moderation
}
Store the raw payload (or the relevant numbers) alongside the user record so you have an audit trail when someone disputes a block.
Queued post-registration screening (best UX). Don't block signup on a third-party lookup. Register the user, dispatch a job, and let the job flag, throttle, or soft-ban:
dispatch(new ScreenNewUser($user))->onQueue('moderation');
The job can retry, fail open, and log the outcome. This removes the API from the synchronous request path entirely — the strongest pattern if you only need moderation, not prevention.
Middleware for guest-writable routes. For comments/contact forms, a small middleware that only runs on POST, only for guests, and skips known-good sessions (e.g. authenticated users, or those with a recent manual approval) keeps cost down:
if ($request->user() || ! $request->isMethod('post')) {
return $next($request);
}
Facade macros for repeated logic. Register a StopForumSpam::isCleanSpammer(...) macro in a service provider so thresholds live in one place rather than scattered across controllers.
Testing. Set STOPFORUMSPAM_ENABLED=false in phpunit.xml so validation rules pass instantly, then write targeted tests that call Http::fake() (or a Guzzle mock) for the actual spam path. Remember cached responses: clear the package's cache keys in setUp if you assert both spam and clean outcomes in the same suite.
Network in the request path. This is a synchronous HTTP call to api.stopforumspam.org. On a bad day that's hundreds of milliseconds, and it will surface as slow registrations. Always combine with the package's cache (frequency in minutes for negative results, expired in days for positive results, per the config), and consider a Guzzle timeout.
Cache staleness cuts both ways. A spammer listed five minutes ago passes until the negative cache expires. A false positive sticks around for the longer expired window. If you have an appeals/unblock flow, purge the package's cache entry for that IP/email when an admin clears a user.
Rate limits are real. StopForumSpam throttles. If you check on every request or every page view, you will get 403/429s. Gate it to write endpoints only, never on reads.
Fail open, and log the failure. Wrap calls in try/catch and treat a timeout or non-200 as "not spam". Blocking every registration because the API hiccupped is a self-inflicted outage. Log the exception with the checked value so you can retroactively sweep users who slipped through.
False positives are normal. Shared hosting IPs, CGNAT, university NAT, Tor exits, and VPNs are all heavily represented in the database. An IP hit alone should almost never be a hard block; email and username hits are far more reliable signals. Also remember $request->ip() is only correct behind a proxy if TrustProxies is configured — otherwise you're screening your load balancer.
IPv6. The database coverage for IPv6 is thinner and the package's value handling differs from IPv4. Test explicitly if your app is dual-stack.
No submission support. The api_key in the config is for reading/attribution; reporting a spammer back to StopForumSpam is a separate POST to /add with username, ip_addr, and email. If you want to contribute, implement it yourself in a queued job — don't expect the package to do it.
Config caching. php artisan config:cache in deployment means STOPFORUMSPAM_* must be present at build time. A missing STOPFORUMSPAM_ENABLED after a config cache refresh will silently flip behaviour.
Customise the failure message. "The email is invalid" is a terrible message for a spam block — it invites retries. Return a generic "registration could not be completed" instead, and log the real reason server-side.
Know what it isn't. This is one signal, not a spam strategy. Pair it with a honeypot field, rate limiting on the registration route, disposable-email blocking (nickurt/laravel-disposable-email from the same author slots in cleanly), and content moderation for anything user-written.
How can I help you explore Laravel packages today?