setono/bot-detection-bundle
A Symfony bundle for detecting bots and crawlers from request data, helping you filter automated traffic in analytics, block unwanted scrapers, and serve tailored responses to human visitors.
Install and register
composer require setono/bot-detection-bundle
With Symfony Flex the bundle is auto-registered in config/bundles.php. Without Flex, add it manually:
// config/bundles.php
Setono\BotDetectionBundle\SetonoBotDetectionBundle::class => ['all' => true],
There is no required configuration — the bundle exists to expose an autowirable detector service, so config/packages/setono_bot_detection.yaml is typically unnecessary. Import the bundle's routes/config only if you actually need to override a service.
Where to look first
vendor/setono/bot-detection-bundle/src/ — it is a small package; read the detector interface and its implementation before wiring anything. Confirm which underlying UA-matching library is pulled in (composer show setono/bot-detection-bundle --requires).bin/console debug:autowiring bot — this gives you the exact FQCN to type-hint in your installed version, instead of copying an interface name from a blog post.bin/console debug:container --show-private | grep -i bot — confirms the service id if you need to decorate or alias it.First use case
The most common day-one use is "stop letting bots pollute my application logic" — analytics events, A/B test assignment, search indexing, recommendation counters. Inject the detector and branch:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// use Setono\BotDetectionBundle\...\BotDetectorInterface; // verify FQCN via debug:autowiring
final class LandingPageController
{
public function __construct(private readonly BotDetectorInterface $botDetector)
{
}
public function __invoke(Request $request): Response
{
$isBot = $this->botDetector->isBot($request->headers->get('User-Agent', ''));
// e.g. don't assign an A/B variant to a crawler
return $this->render('landing.html.twig', ['isBot' => $isBot]);
}
}
Validate the install in ten seconds with a throwaway console command that runs a known Googlebot UA and a known Chrome UA through the detector. If both return the same value, check that the service you injected is the real implementation and not a stub.
Tag the request once, in a listener. Don't call the detector from five different services. Add a kernel.request subscriber that sets a request attribute, then read it downstream:
final class BotFlagSubscriber implements EventSubscriberInterface
{
public function __construct(private readonly BotDetectorInterface $botDetector) {}
public static function getSubscribedEvents(): array
{
return [KernelEvents::REQUEST => ['onRequest', 100]];
}
public function onRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$ua = $event->getRequest()->headers->get('User-Agent', '');
$event->getRequest()->attributes->set('_is_bot', $this->botDetector->isBot($ua));
}
}
Gate expensive or stateful work. The classic trio: skip recommendation/recently-viewed writes, skip A/B variant persistence, skip server-side analytics hit dispatch. Guarding these is where the bundle pays for itself — bot traffic routinely dwarfs human traffic on public pages.
Combine with symfony/rate-limiter for a cheap bot-tuned limit. Give flagged UAs a materially tighter limiter, but keep it as a limit, not a block — see the gotchas.
SEO-safe allow-listing. Most teams need the inverse of "block bots": let Googlebot/Bingbot through to server-rendered content while treating everything else as a crawler. Build the allow-list yourself (the bundle is a boolean UA check, not a policy engine) and, for the search engines you actually care about, verify by reverse-DNS/IP rather than UA string.
Decorate for memoization and policy. If the detector runs in more than one listener, wrap it:
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
#[AsDecorator(decorates: 'setono_bot_detection.bot_detector')] // confirm the id
final class CachingBotDetector implements BotDetectorInterface
{
/** @var array<string, bool> */
private array $cache = [];
public function __construct(private readonly BotDetectorInterface $inner) {}
public function isBot(string $userAgent): bool
{
return $this->cache[$userAgent] ??= $this->inner->isBot($userAgent);
}
}
Decoration is the intended seam — you can also layer an internal-traffic allow-list or an office-IP bypass here without touching vendor code.
Logging and metrics. Add a Monolog processor that stamps is_bot onto every record, or increment a counter. This is exactly the telemetry you want when someone asks "why are our conversion numbers weird".
Deterministic tests. Bind a fake implementation in config/services_test.yaml so functional tests never depend on UA-string heuristics:
services:
_defaults: { autowire: true, public: true }
Setono\BotDetectionBundle\...\BotDetectorInterface:
class: Tests\Fake\AlwaysHumanBotDetector
Unit-test your own policy code against the interface; leave the regex library's accuracy to the library.
Googlebot/2.1. Never use this for authorization, access control, or deciding whether to serve sensitive data. Use it for analytics hygiene, cache behaviour, rate-limit tuning, and UX.composer update. New crawlers slip through on stale installs; add a periodic dependency-update review if bot detection is load-bearing for your metrics.false (human), and many scrapers send no UA at all. Decide your policy explicitly: if ($ua === '') { /* treat as bot / unknown */ } rather than inheriting the library default.Vary: User-Agent shatters your HTTP cache into thousands of entries. Prefer deciding before the response is stored (a Cache-Control/no-store decision), or branch in application code after the cached fragment is served.getMatches() equivalent), log it alongside the UA. "False positive on a real user" is impossible to diagnose from a bare boolean; the matched regex plus the UA gets you a one-line bug report.composer.json constraints rather than assuming a recent release date implies compatibility with your target Symfony version.How can I help you explore Laravel packages today?