mchev/banhammer
Laravel package for banning users, IP addresses, and other entities with a polymorphic ban model, middleware, events, and helper methods. Supports temporary or permanent bans, reasons, and easy integration into existing applications.
composer require mchev/banhammer, then publish and run the migrations (php artisan vendor:publish → provider/tag, php artisan migrate). The package ships its own bans table; review the migration before running it in production.Bannable trait to any model you want to be bannable — in most apps that's App\Models\User, but it works on any Eloquent model.bootstrap/app.php (->withMiddleware(...)); on older skeletons it's app/Http/Kernel.php. Put it early in the global or web stack so banned IPs are rejected before session/auth work happens.config/banhammer.php first — that's where cache behaviour, table naming, and the middleware/route toggles live. It's the single best map of the package's surface area.php artisan list | grep -i ban rather than trusting docs; the ban/unban/list commands are the fastest way to validate your install end-to-end.On v2.5.0: this release is purely compatibility — Laravel 13.x support plus a PHP 8.5 test matrix entry. If you're on Laravel 11/12 and PHP 8.2–8.4, nothing changes for you. It does mean the package is safe to carry forward through a Laravel 13 upgrade, which is worth noting if you're planning one.
Model-level bans (the common case). With the trait on User, you get a fluent API for banning, unbanning, and checking status. The usual shape is:
$user->ban(['reason' => 'spam', 'expired_at' => now()->addDays(7)]);
$user->unban();
$user->isBanned();
Wrap this in a small BanService rather than calling it from controllers — you'll want one place to dispatch audit events, notify moderators, and write to your own log table.
IP bans for unauthenticated traffic. The middleware is what actually enforces bans on guests, since there's no user to check. Keep a separate admin action for "ban this IP" that captures the request IP plus a reason, so you can trace why an address was blocked six months later.
Temporary bans and expiry. Prefer setting an expiry over manual unbanning — it makes support tickets self-resolving. Whatever you do, don't assume expired rows vanish on their own: verify whether enforcement is a query scope or a cleanup job, and if it's the latter, schedule it (Schedule::command(...)->hourly() or a queued job). Add a test that a ban with a past expired_at no longer blocks access.
Country / geo-based bans. If you use country banning, it depends on a GeoIP lookup driver and a local database file. Treat this as an optional layer: it adds a dependency, a data file you must keep updated, and latency on the request path. Cache the country resolution per IP if you enable it on high-traffic routes.
Moderation UI integration. A typical pattern is a BansController with store/destroy, a BanPolicy, and a Banned / Unbanned event listener that writes to an audit_logs table and pings Slack. The package's events are the intended extension point here — listen rather than subclass.
Auth integration. Pair bans with login throttling rather than replacing it. A banned user should get a generic rejection (don't leak "you are banned" to an attacker probing accounts); a banned IP should get a flat 403 from middleware.
Testing. Use RefreshDatabase and be explicit about IPs: $this->withServerVariables(['REMOTE_ADDR' => '203.0.113.10']) for middleware tests, and $this->withHeaders([...]) if you're behind a trusted proxy setup. Add a banned() factory state for convenience. Flush the ban cache in setUp() — cached ban lists leaking between tests is the single most common source of flaky suites with this package.
StartSession, Authenticate, and anything that writes to the DB. A banned IP should cost you one cache read, not a full session bootstrap.REMOTE_ADDR is the proxy. You must have TrustProxies configured with the correct ranges, or every ban will match the wrong address (or nothing). Test from a real client IP, not just localhost.127.0.0.1 and ::1 are not the same ban. IPv4 vs IPv6 mismatches are a frequent "the ban doesn't work" bug report. Ban both forms, and check whether CIDR ranges are supported before promising subnet bans to anyone.expired_at. If cleanup is a separate concern, schedule it yourself and monitor it.Ban model. Avoid forking the package for behaviour you can get from a listener.composer update, since the new constraint set may pull you forward. Run your middleware tests after upgrading to confirm proxy/IP behaviour is unchanged.How can I help you explore Laravel packages today?