Introduction
Integrate Botbye bot protection into your Symfony application using event subscribers. This guide demonstrates how to protect your Symfony routes with dependency injection and event-driven architecture.
Installation
Install the SDK via Composer:
1
composer require botbye/botbye-php-sdk
Configuration
Configure the Botbye client as a service in config/services.yaml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
services:
_defaults:
autowire: true
autoconfigure: true
# Botbye Configuration
Botbye\Client\BotbyeConfig:
arguments:
# Use your project server-key
$serverKey: '00000000-0000-0000-0000-000000000000'
# Botbye Client
Botbye\Client\BotbyeClient:
arguments:
$config: '@Botbye\Client\BotbyeConfig'
# Event Subscriber
App\EventSubscriber\BotbyeSubscriber:
tags:
- { name: kernel.event_subscriber }
Usage
Event Subscriber Implementation
1. Create an event subscriber to validate requests:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?php
namespace App\EventSubscriber;
use Botbye\Client\BotbyeClient;
use Botbye\Model\ConnectionDetails;
use Botbye\Model\Headers;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
class BotbyeSubscriber implements EventSubscriberInterface
{
public function __construct(
private BotbyeClient $botbye
) {
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 10],
];
}
public function onKernelRequest(RequestEvent $event): void
{
// Only validate main requests, not sub-requests
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
// Skip validation for specific routes (e.g., health checks)
if ($this->shouldSkipValidation($request->getPathInfo())) {
return;
}
$connectionDetails = new ConnectionDetails(
remoteAddr: $request->getClientIp(),
requestMethod: $request->getMethod(),
requestUri: $request->getRequestUri(),
serverPort: $request->getPort(),
serverName: $request->getHost()
);
$headers = Headers::fromArray($request->headers->all());
// Get token from header or any place you store it.
// For example in "x-botbye-token" header
$token = $request->headers->get('x-botbye-token');
$response = $this->botbye->validateRequest(
token: $token,
connectionDetails: $connectionDetails,
headers: $headers,
customFields: [
'session_id' => $request->getSession()->getId(),
'route' => $request->attributes->get('_route'),
]
);
if ($response->result !== null && !$response->result->isAllowed) {
throw new AccessDeniedHttpException('Access denied by Botbye protection');
}
}
private function shouldSkipValidation(string $path): bool
{
$skipPaths = [
'/health',
'/metrics',
'/_profiler',
];
foreach ($skipPaths as $skipPath) {
if (str_starts_with($path, $skipPath)) {
return true;
}
}
return false;
}
}
2. For more granular control, use route attributes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
namespace App\Controller;
use App\Attribute\BotbyeProtected;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
class ApiController extends AbstractController
{
#[Route('/api/checkout', methods: ['POST'])]
#[BotbyeProtected]
public function checkout(): JsonResponse
{
// Your checkout logic
return $this->json(['status' => 'success']);
}
#[Route('/api/login', methods: ['POST'])]
#[BotbyeProtected]
public function login(): JsonResponse
{
// Your login logic
return $this->json(['status' => 'success']);
}
}
Create the attribute class:
1
2
3
4
5
6
7
8
<?php
namespace App\Attribute;
#[\Attribute(\Attribute::TARGET_METHOD)]
class BotbyeProtected
{
}
Update the subscriber to check for the attribute:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php
namespace App\EventSubscriber;
use App\Attribute\BotbyeProtected;
use Botbye\Client\BotbyeClient;
use Botbye\Model\ConnectionDetails;
use Botbye\Model\Headers;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
class BotbyeSubscriber implements EventSubscriberInterface
{
public function __construct(
private BotbyeClient $botbye
) {
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER => 'onKernelController',
];
}
public function onKernelController(ControllerEvent $event): void
{
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
$method = new \ReflectionMethod($controller[0], $controller[1]);
$attributes = $method->getAttributes(BotbyeProtected::class);
if (empty($attributes)) {
return;
}
$request = $event->getRequest();
$connectionDetails = new ConnectionDetails(
remoteAddr: $request->getClientIp(),
requestMethod: $request->getMethod(),
requestUri: $request->getRequestUri()
);
$headers = Headers::fromArray($request->headers->all());
// Get token from header or any place you store it.
// For example in "x-botbye-token" header
$token = $request->headers->get('x-botbye-token');
$response = $this->botbye->validateRequest(
token: $token,
connectionDetails: $connectionDetails,
headers: $headers
);
if ($response->result !== null && !$response->result->isAllowed) {
throw new AccessDeniedHttpException('Access denied by Botbye protection');
}
}
}
Advanced Configuration
Custom HTTP Client
Configure a custom HTTP client:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# config/services.yaml
services:
# Custom HTTP Client for Botbye
botbye.http_client:
class: Symfony\Component\HttpClient\HttpClient
factory: ['Symfony\Component\HttpClient\HttpClient', 'create']
arguments:
-
timeout: 1
max_redirects: 0
# Botbye Client with custom HTTP client
Botbye\Client\BotbyeClient:
arguments:
$config: '@Botbye\Client\BotbyeConfig'
$httpClient: '@botbye.http_client'
Logging Integration
Integrate with Symfony's Monolog:
1
2
3
4
5
6
7
8
9
10
11
# config/packages/monolog.yaml
monolog:
channels:
- botbye
handlers:
botbye:
type: stream
path: '%kernel.logs_dir%/botbye.log'
level: warning
channels: [botbye]
Configure the service:
1
2
3
4
5
6
7
# config/services.yaml
services:
Botbye\Client\BotbyeClient:
arguments:
$config: '@Botbye\Client\BotbyeConfig'
$httpClient: null
$logger: '@monolog.logger.botbye'
Best Practices
1. Service Configuration - Use Symfony's dependency injection for clean architecture
2. Event Priorities - Set appropriate event priorities to control execution order
3. Selective Protection - Use attributes or route patterns to protect specific endpoints
4. Logging - Use Monolog channels for organized logging
5. Environment Configuration - Use different configurations for dev/prod environments
Testing
Mock the Botbye client in your tests:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
namespace App\Tests\Controller;
use Botbye\Client\BotbyeClient;
use Botbye\Model\BotbyeResponse;
use Botbye\Model\BotbyeResult;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ApiControllerTest extends WebTestCase
{
public function testCheckoutAllowed(): void
{
$client = static::createClient();
// Mock Botbye client
$botbyeMock = $this->createMock(BotbyeClient::class);
$botbyeMock->method('validateRequest')
->willReturn(new BotbyeResponse(
result: new BotbyeResult(isAllowed: true),
error: null
));
$client->getContainer()->set(BotbyeClient::class, $botbyeMock);
$client->request('POST', '/api/checkout', [], [], [], json_encode([
'item' => 'test',
]));
$this->assertResponseIsSuccessful();
}
public function testCheckoutBlocked(): void
{
$client = static::createClient();
// Mock Botbye client
$botbyeMock = $this->createMock(BotbyeClient::class);
$botbyeMock->method('validateRequest')
->willReturn(new BotbyeResponse(
result: new BotbyeResult(isAllowed: false),
error: null
));
$client->getContainer()->set(BotbyeClient::class, $botbyeMock);
$client->request('POST', '/api/checkout', [], [], [], json_encode([
'item' => 'test',
]));
$this->assertResponseStatusCodeSame(403);
}
}
Examples of BotBye API responses
Bot detected:
1
2
3
4
5
6
7
{
"reqId": "f77b2abd-c5d7-44f0-be4f-174b04876583",
"result": {
"isAllowed": false
},
"error": "Automation tool used"
}
Bot not detected:
1
2
3
4
5
6
7
{
"reqId": "f77b2abd-c5d7-44f0-be4f-174b04876583",
"result": {
"isAllowed": true
},
"error": null
}
Request banned by custom rule:
1
2
3
4
5
6
7
8
9
{
"reqId": "f77b2abd-c5d7-44f0-be4f-174b04876583",
"result": {
"isAllowed": false
},
"error": {
"message": "Banned by rule: MY_CUSTOM_RULE"
}
}
Invalid server-key:
1
2
3
4
5
6
7
{
"reqId": "f77b2abd-c5d7-44f0-be4f-174b04876583",
"result": null,
"error": {
"message": "[BotBye] Bad Request: Invalid Server Key"
}
}