Symfony
Symfony

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"
  }
}