Skip to content

Symfony Integration

Rumen Damyanov edited this page Jul 31, 2025 · 1 revision

Symfony Integration Guide

Complete guide to integrating php-geolocation with Symfony applications using services, event listeners, and best practices.

Table of Contents

Installation & Setup

Install Package

composer require rumenx/geolocation

Create Configuration

Create config/packages/geolocation.yaml:

# config/packages/geolocation.yaml
parameters:
    geolocation.country_to_language:
        US: ['en']
        CA: ['en', 'fr']
        GB: ['en']
        AU: ['en']
        DE: ['de']
        AT: ['de']
        CH: ['de', 'fr', 'it']
        FR: ['fr']
        BE: ['fr', 'nl']
        ES: ['es']
        MX: ['es']
        AR: ['es']
        BR: ['pt']
        PT: ['pt']
        JP: ['ja']
        CN: ['zh']
        RU: ['ru']
        IT: ['it']
        NL: ['nl']
        SE: ['sv']
        NO: ['no']
        DK: ['da']
        FI: ['fi']

    geolocation.available_languages: ['en', 'fr', 'de', 'es']
    geolocation.default_language: 'en'
    geolocation.cookie_name: 'app_language'
    geolocation.cookie_lifetime: 2592000  # 30 days
    geolocation.enable_simulation: '%kernel.debug%'

services:
    Rumenx\Geolocation\Geolocation:
        arguments:
            $server: '@request_stack'
            $countryToLanguage: '%geolocation.country_to_language%'
            $languageCookieName: '%geolocation.cookie_name%'
        public: true

    App\Service\GeolocationService:
        arguments:
            $geolocation: '@Rumenx\Geolocation\Geolocation'
            $availableLanguages: '%geolocation.available_languages%'
            $defaultLanguage: '%geolocation.default_language%'

Service Configuration

Geolocation Service

<?php
// src/Service/GeolocationService.php

namespace App\Service;

use Rumenx\Geolocation\Geolocation;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

class GeolocationService
{
    private Geolocation $geolocation;
    private array $availableLanguages;
    private string $defaultLanguage;
    private RequestStack $requestStack;

    public function __construct(
        Geolocation $geolocation,
        array $availableLanguages,
        string $defaultLanguage,
        RequestStack $requestStack
    ) {
        $this->geolocation = $geolocation;
        $this->availableLanguages = $availableLanguages;
        $this->defaultLanguage = $defaultLanguage;
        $this->requestStack = $requestStack;
    }

    public function getCountryCode(): ?string
    {
        return $this->geolocation->getCountryCode();
    }

    public function getLanguageForCountry(?string $country = null): ?string
    {
        $country = $country ?? $this->getCountryCode();
        return $this->geolocation->getLanguageForCountry($country, $this->availableLanguages);
    }

    public function detectUserLanguage(): string
    {
        $request = $this->requestStack->getCurrentRequest();

        if (!$request) {
            return $this->defaultLanguage;
        }

        // 1. Check URL parameter
        if ($request->query->has('lang')) {
            $lang = $request->query->get('lang');
            if (in_array($lang, $this->availableLanguages)) {
                return $lang;
            }
        }

        // 2. Check cookie
        if ($request->cookies->has('app_language')) {
            $lang = $request->cookies->get('app_language');
            if (in_array($lang, $this->availableLanguages)) {
                return $lang;
            }
        }

        // 3. Use geolocation
        $detectedLang = $this->getLanguageForCountry();
        if ($detectedLang && in_array($detectedLang, $this->availableLanguages)) {
            return $detectedLang;
        }

        return $this->defaultLanguage;
    }

    public function getGeoInfo(array $fields = []): array
    {
        return $this->geolocation->getGeoInfo($fields);
    }

    public function isLocalDevelopment(): bool
    {
        return $this->geolocation->isLocalDevelopment();
    }

    public function shouldSetLanguageCookie(): bool
    {
        return $this->geolocation->shouldSetLanguage();
    }

    public function simulateCountry(string $country): self
    {
        $simulatedGeo = Geolocation::simulate($country);

        return new self(
            $simulatedGeo,
            $this->availableLanguages,
            $this->defaultLanguage,
            $this->requestStack
        );
    }
}

Language Detection Service

<?php
// src/Service/LanguageDetectionService.php

namespace App\Service;

use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class LanguageDetectionService
{
    private GeolocationService $geoService;
    private array $availableLanguages;
    private string $cookieName;
    private int $cookieLifetime;

    public function __construct(
        GeolocationService $geoService,
        array $availableLanguages,
        string $cookieName,
        int $cookieLifetime
    ) {
        $this->geoService = $geoService;
        $this->availableLanguages = $availableLanguages;
        $this->cookieName = $cookieName;
        $this->cookieLifetime = $cookieLifetime;
    }

    public function detectLanguage(Request $request): string
    {
        // Priority order: URL param → Cookie → Geolocation → Default
        return $this->detectFromUrl($request)
            ?? $this->detectFromCookie($request)
            ?? $this->detectFromGeolocation()
            ?? $this->availableLanguages[0];
    }

    public function setLanguageCookie(Response $response, string $language): void
    {
        if (in_array($language, $this->availableLanguages)) {
            $cookie = Cookie::create(
                $this->cookieName,
                $language,
                time() + $this->cookieLifetime,
                '/',
                null,
                false,
                true // httpOnly
            );

            $response->headers->setCookie($cookie);
        }
    }

    public function createLanguageResponse(string $language): Response
    {
        $response = new Response();
        $this->setLanguageCookie($response, $language);
        return $response;
    }

    private function detectFromUrl(Request $request): ?string
    {
        $lang = $request->query->get('lang');
        return (is_string($lang) && in_array($lang, $this->availableLanguages)) ? $lang : null;
    }

    private function detectFromCookie(Request $request): ?string
    {
        $lang = $request->cookies->get($this->cookieName);
        return (is_string($lang) && in_array($lang, $this->availableLanguages)) ? $lang : null;
    }

    private function detectFromGeolocation(): ?string
    {
        $lang = $this->geoService->detectUserLanguage();
        return in_array($lang, $this->availableLanguages) ? $lang : null;
    }
}

Event Listeners

Kernel Request Listener

<?php
// src/EventListener/GeolocationListener.php

namespace App\EventListener;

use App\Service\GeolocationService;
use App\Service\LanguageDetectionService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class GeolocationListener implements EventSubscriberInterface
{
    private GeolocationService $geoService;
    private LanguageDetectionService $languageService;

    public function __construct(
        GeolocationService $geoService,
        LanguageDetectionService $languageService
    ) {
        $this->geoService = $geoService;
        $this->languageService = $languageService;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::REQUEST => [
                ['onKernelRequest', 20], // High priority to run early
            ],
        ];
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        $request = $event->getRequest();

        // Only process master requests
        if (!$event->isMainRequest()) {
            return;
        }

        // Skip for API routes or admin routes if needed
        if ($this->shouldSkipGeolocation($request)) {
            return;
        }

        // Detect and set language
        $language = $this->languageService->detectLanguage($request);
        $request->setLocale($language);

        // Add geolocation data to request attributes
        $geoInfo = $this->geoService->getGeoInfo();
        $request->attributes->set('geo_country', $geoInfo['country_code'] ?? null);
        $request->attributes->set('geo_language', $language);
        $request->attributes->set('geo_info', $geoInfo);
        $request->attributes->set('is_development', $this->geoService->isLocalDevelopment());
    }

    private function shouldSkipGeolocation(Request $request): bool
    {
        $path = $request->getPathInfo();

        // Skip for API routes
        if (str_starts_with($path, '/api/')) {
            return true;
        }

        // Skip for admin routes
        if (str_starts_with($path, '/admin/')) {
            return true;
        }

        // Skip for asset files
        if (preg_match('/\.(css|js|png|jpg|gif|ico|svg|woff|woff2|ttf)$/', $path)) {
            return true;
        }

        return false;
    }
}

Response Listener

<?php
// src/EventListener/LanguageCookieListener.php

namespace App\EventListener;

use App\Service\GeolocationService;
use App\Service\LanguageDetectionService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class LanguageCookieListener implements EventSubscriberInterface
{
    private GeolocationService $geoService;
    private LanguageDetectionService $languageService;

    public function __construct(
        GeolocationService $geoService,
        LanguageDetectionService $languageService
    ) {
        $this->geoService = $geoService;
        $this->languageService = $languageService;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::RESPONSE => ['onKernelResponse', -10], // Low priority
        ];
    }

    public function onKernelResponse(ResponseEvent $event): void
    {
        $request = $event->getRequest();
        $response = $event->getResponse();

        if (!$event->isMainRequest()) {
            return;
        }

        // Set language cookie if it should be set
        if ($this->geoService->shouldSetLanguageCookie()) {
            $detectedLanguage = $this->geoService->detectUserLanguage();
            $this->languageService->setLanguageCookie($response, $detectedLanguage);
        }

        // Add geolocation headers for debugging (development only)
        if ($this->geoService->isLocalDevelopment()) {
            $geoInfo = $this->geoService->getGeoInfo();
            $response->headers->set('X-Geo-Country', $geoInfo['country_code'] ?? 'unknown');
            $response->headers->set('X-Geo-Language', $request->getLocale());
            $response->headers->set('X-Geo-Development', 'true');
        }
    }
}

Service Registration

# config/services.yaml
services:
    App\Service\GeolocationService:
        arguments:
            $availableLanguages: '%geolocation.available_languages%'
            $defaultLanguage: '%geolocation.default_language%'

    App\Service\LanguageDetectionService:
        arguments:
            $availableLanguages: '%geolocation.available_languages%'
            $cookieName: '%geolocation.cookie_name%'
            $cookieLifetime: '%geolocation.cookie_lifetime%'

    App\EventListener\GeolocationListener:
        tags:
            - { name: kernel.event_subscriber }

    App\EventListener\LanguageCookieListener:
        tags:
            - { name: kernel.event_subscriber }

Controller Integration

Base Controller

<?php
// src/Controller/BaseController.php

namespace App\Controller;

use App\Service\GeolocationService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

abstract class BaseController extends AbstractController
{
    protected GeolocationService $geoService;

    public function __construct(GeolocationService $geoService)
    {
        $this->geoService = $geoService;
    }

    protected function getGeoData(): array
    {
        return [
            'country' => $this->geoService->getCountryCode(),
            'language' => $this->geoService->detectUserLanguage(),
            'is_development' => $this->geoService->isLocalDevelopment(),
        ];
    }

    protected function renderWithGeo(string $view, array $parameters = []): Response
    {
        return $this->render($view, array_merge($parameters, [
            'geo' => $this->getGeoData(),
        ]));
    }
}

Home Controller

<?php
// src/Controller/HomeController.php

namespace App\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class HomeController extends BaseController
{
    #[Route('/', name: 'app_home')]
    public function index(Request $request): Response
    {
        $geoInfo = $this->geoService->getGeoInfo();

        return $this->renderWithGeo('home/index.html.twig', [
            'geo_info' => $geoInfo,
            'user_country' => $request->attributes->get('geo_country'),
            'detected_language' => $request->attributes->get('geo_language'),
        ]);
    }

    #[Route('/country/{country}', name: 'app_simulate_country')]
    public function simulateCountry(string $country): Response
    {
        // For development/testing only
        if (!$this->geoService->isLocalDevelopment()) {
            throw $this->createNotFoundException('Simulation only available in development');
        }

        $simulatedGeo = $this->geoService->simulateCountry($country);
        $geoInfo = $simulatedGeo->getGeoInfo();

        return $this->render('home/simulation.html.twig', [
            'simulated_country' => $country,
            'geo_info' => $geoInfo,
            'geo' => [
                'country' => $country,
                'language' => $simulatedGeo->detectUserLanguage(),
                'is_development' => true,
            ],
        ]);
    }
}

API Controller

<?php
// src/Controller/Api/GeolocationController.php

namespace App\Controller\Api;

use App\Service\GeolocationService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

#[Route('/api/geolocation', name: 'api_geolocation_')]
class GeolocationController extends AbstractController
{
    private GeolocationService $geoService;

    public function __construct(GeolocationService $geoService)
    {
        $this->geoService = $geoService;
    }

    #[Route('/detect', name: 'detect', methods: ['GET'])]
    public function detect(Request $request): JsonResponse
    {
        try {
            $geoInfo = $this->geoService->getGeoInfo();

            return $this->json([
                'success' => true,
                'data' => [
                    'country_code' => $geoInfo['country_code'] ?? null,
                    'language' => $this->geoService->detectUserLanguage(),
                    'ip' => $geoInfo['ip'] ?? null,
                    'is_development' => $this->geoService->isLocalDevelopment(),
                    'geo_info' => $geoInfo,
                ],
            ]);
        } catch (\Exception $e) {
            return $this->json([
                'success' => false,
                'error' => 'Geolocation detection failed',
                'message' => $e->getMessage(),
            ], 500);
        }
    }

    #[Route('/countries', name: 'countries', methods: ['GET'])]
    public function getSupportedCountries(): JsonResponse
    {
        // Return list of supported countries for simulation
        $countries = [
            'US' => 'United States',
            'CA' => 'Canada',
            'GB' => 'United Kingdom',
            'DE' => 'Germany',
            'FR' => 'France',
            'ES' => 'Spain',
            'JP' => 'Japan',
            'BR' => 'Brazil',
        ];

        return $this->json([
            'success' => true,
            'data' => $countries,
        ]);
    }

    #[Route('/simulate/{country}', name: 'simulate', methods: ['POST'])]
    public function simulate(string $country): JsonResponse
    {
        if (!$this->geoService->isLocalDevelopment()) {
            return $this->json([
                'success' => false,
                'error' => 'Simulation only available in development mode',
            ], 403);
        }

        try {
            $simulatedGeo = $this->geoService->simulateCountry($country);
            $geoInfo = $simulatedGeo->getGeoInfo();

            return $this->json([
                'success' => true,
                'data' => [
                    'simulated_country' => $country,
                    'geo_info' => $geoInfo,
                    'detected_language' => $simulatedGeo->detectUserLanguage(),
                ],
            ]);
        } catch (\Exception $e) {
            return $this->json([
                'success' => false,
                'error' => 'Simulation failed',
                'message' => $e->getMessage(),
            ], 500);
        }
    }
}

Language Controller

<?php
// src/Controller/LanguageController.php

namespace App\Controller;

use App\Service\LanguageDetectionService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

#[Route('/language', name: 'app_language_')]
class LanguageController extends AbstractController
{
    private LanguageDetectionService $languageService;

    public function __construct(LanguageDetectionService $languageService)
    {
        $this->languageService = $languageService;
    }

    #[Route('/set/{language}', name: 'set')]
    public function setLanguage(string $language, Request $request): RedirectResponse
    {
        $response = $this->languageService->createLanguageResponse($language);

        // Redirect back to previous page
        $referer = $request->headers->get('referer');
        $redirectUrl = $referer ?: $this->generateUrl('app_home');

        return $this->redirect($redirectUrl, 302, $response->headers->all());
    }

    #[Route('/auto-detect', name: 'auto_detect')]
    public function autoDetect(Request $request): RedirectResponse
    {
        $detectedLanguage = $this->languageService->detectLanguage($request);

        return $this->redirectToRoute('app_language_set', [
            'language' => $detectedLanguage,
        ]);
    }
}

Twig Extensions

Geolocation Extension

<?php
// src/Twig/GeolocationExtension.php

namespace App\Twig;

use App\Service\GeolocationService;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

class GeolocationExtension extends AbstractExtension
{
    private GeolocationService $geoService;

    public function __construct(GeolocationService $geoService)
    {
        $this->geoService = $geoService;
    }

    public function getFunctions(): array
    {
        return [
            new TwigFunction('geo_country', [$this, 'getCountry']),
            new TwigFunction('geo_language', [$this, 'getLanguage']),
            new TwigFunction('geo_info', [$this, 'getGeoInfo']),
            new TwigFunction('geo_is_development', [$this, 'isDevelopment']),
            new TwigFunction('geo_simulate', [$this, 'simulateCountry']),
        ];
    }

    public function getCountry(): ?string
    {
        return $this->geoService->getCountryCode();
    }

    public function getLanguage(): string
    {
        return $this->geoService->detectUserLanguage();
    }

    public function getGeoInfo(array $fields = []): array
    {
        return $this->geoService->getGeoInfo($fields);
    }

    public function isDevelopment(): bool
    {
        return $this->geoService->isLocalDevelopment();
    }

    public function simulateCountry(string $country): array
    {
        if (!$this->geoService->isLocalDevelopment()) {
            return [];
        }

        $simulatedGeo = $this->geoService->simulateCountry($country);
        return $simulatedGeo->getGeoInfo();
    }
}

Twig Templates

{# templates/base.html.twig #}
<!DOCTYPE html>
<html lang="{{ app.request.locale }}">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}Welcome{% endblock %}</title>

    {% if geo_is_development() %}
        <meta name="geo-development" content="true">
        <meta name="geo-country" content="{{ geo_country() }}">
        <meta name="geo-language" content="{{ geo_language() }}">
    {% endif %}
</head>
<body>
    {% if geo_is_development() %}
        <div class="dev-banner">
            <strong>Development Mode:</strong>
            Country: {{ geo_country() ?? 'Unknown' }} |
            Language: {{ geo_language() }} |
            <a href="{{ path('app_language_auto_detect') }}">Auto-detect language</a>
        </div>
    {% endif %}

    <nav>
        <div class="language-switcher">
            <select onchange="window.location.href='/language/set/' + this.value">
                <option value="en" {{ app.request.locale == 'en' ? 'selected' : '' }}>English</option>
                <option value="fr" {{ app.request.locale == 'fr' ? 'selected' : '' }}>Français</option>
                <option value="de" {{ app.request.locale == 'de' ? 'selected' : '' }}>Deutsch</option>
                <option value="es" {{ app.request.locale == 'es' ? 'selected' : '' }}>Español</option>
            </select>
        </div>
    </nav>

    <main>
        {% block body %}{% endblock %}
    </main>

    {% block javascripts %}
        {% if geo_is_development() %}
            <script>
                console.log('Geolocation Info:', {{ geo_info()|json_encode|raw }});
            </script>
        {% endif %}
    {% endblock %}
</body>
</html>
{# templates/home/index.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}Welcome from {{ geo_country() ?? 'Unknown' }}{% endblock %}

{% block body %}
    <h1>
        {% if geo_country() == 'US' %}
            🇺🇸 Welcome to our US visitors!
        {% elseif geo_country() == 'DE' %}
            🇩🇪 Willkommen in Deutschland!
        {% elseif geo_country() == 'FR' %}
            🇫🇷 Bienvenue en France!
        {% elseif geo_country() == 'ES' %}
            🇪🇸 ¡Bienvenido a España!
        {% else %}
            🌍 Welcome to our international visitors!
        {% endif %}
    </h1>

    <div class="geo-info">
        <h2>Your Information</h2>
        <ul>
            <li><strong>Country:</strong> {{ geo_country() ?? 'Unknown' }}</li>
            <li><strong>Language:</strong> {{ geo_language() }}</li>
            <li><strong>Development Mode:</strong> {{ geo_is_development() ? 'Yes' : 'No' }}</li>
        </ul>
    </div>

    {% if geo_is_development() %}
        <div class="simulation-tools">
            <h3>Test Different Countries</h3>
            <div class="country-buttons">
                <a href="{{ path('app_simulate_country', {country: 'US'}) }}" class="btn">🇺🇸 USA</a>
                <a href="{{ path('app_simulate_country', {country: 'DE'}) }}" class="btn">🇩🇪 Germany</a>
                <a href="{{ path('app_simulate_country', {country: 'FR'}) }}" class="btn">🇫🇷 France</a>
                <a href="{{ path('app_simulate_country', {country: 'JP'}) }}" class="btn">🇯🇵 Japan</a>
            </div>
        </div>
    {% endif %}
{% endblock %}

Console Commands

Geolocation Test Command

<?php
// src/Command/GeolocationTestCommand.php

namespace App\Command;

use App\Service\GeolocationService;
use Rumenx\Geolocation\Geolocation;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'app:geo:test',
    description: 'Test geolocation functionality',
)]
class GeolocationTestCommand extends Command
{
    private GeolocationService $geoService;

    public function __construct(GeolocationService $geoService)
    {
        $this->geoService = $geoService;
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addArgument('country', InputArgument::OPTIONAL, 'Country code to simulate')
            ->setHelp('Test geolocation detection or simulate a specific country');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $country = $input->getArgument('country');

        if ($country) {
            return $this->testSimulation($io, $country);
        } else {
            return $this->testCurrent($io);
        }
    }

    private function testCurrent(SymfonyStyle $io): int
    {
        $io->title('Current Geolocation Test');

        $geoInfo = $this->geoService->getGeoInfo();

        $io->table(
            ['Property', 'Value'],
            [
                ['Country', $geoInfo['country_code'] ?? 'Not detected'],
                ['Language', $this->geoService->detectUserLanguage()],
                ['IP', $geoInfo['ip'] ?? 'Not available'],
                ['Development Mode', $this->geoService->isLocalDevelopment() ? 'Yes' : 'No'],
                ['Browser', json_encode($geoInfo['browser'] ?? [])],
                ['OS', $geoInfo['os'] ?? 'Not detected'],
                ['Device', $geoInfo['device'] ?? 'Not detected'],
            ]
        );

        return Command::SUCCESS;
    }

    private function testSimulation(SymfonyStyle $io, string $country): int
    {
        $io->title("Simulation Test for {$country}");

        try {
            $simulatedGeo = $this->geoService->simulateCountry($country);
            $geoInfo = $simulatedGeo->getGeoInfo();

            $io->table(
                ['Property', 'Value'],
                collect($geoInfo)->map(function ($value, $key) {
                    return [$key, is_array($value) ? json_encode($value) : (string) $value];
                })->toArray()
            );

            $io->success("Simulation for {$country} completed successfully");
            return Command::SUCCESS;
        } catch (\Exception $e) {
            $io->error("Simulation failed: " . $e->getMessage());
            return Command::FAILURE;
        }
    }
}

Language Detection Command

<?php
// src/Command/LanguageDetectionCommand.php

namespace App\Command;

use App\Service\LanguageDetectionService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(
    name: 'app:language:test',
    description: 'Test language detection logic',
)]
class LanguageDetectionCommand extends Command
{
    private LanguageDetectionService $languageService;

    public function __construct(LanguageDetectionService $languageService)
    {
        $this->languageService = $languageService;
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        $io->title('Language Detection Test');

        $testCases = [
            ['country' => 'US', 'accept_lang' => 'en-US,en;q=0.9', 'expected' => 'en'],
            ['country' => 'DE', 'accept_lang' => 'de-DE,de;q=0.9,en;q=0.8', 'expected' => 'de'],
            ['country' => 'FR', 'accept_lang' => 'fr-FR,fr;q=0.9,en;q=0.8', 'expected' => 'fr'],
            ['country' => 'CA', 'accept_lang' => 'fr-CA,fr;q=0.9,en;q=0.8', 'expected' => 'fr'],
            ['country' => 'CH', 'accept_lang' => 'de-CH,de;q=0.9,fr;q=0.8', 'expected' => 'de'],
        ];

        $results = [];
        foreach ($testCases as $case) {
            // Simulate environment for each test case
            $_SERVER['HTTP_CF_IPCOUNTRY'] = $case['country'];
            $_SERVER['HTTP_ACCEPT_LANGUAGE'] = $case['accept_lang'];

            // Create mock request for testing
            $mockRequest = new \Symfony\Component\HttpFoundation\Request();
            $mockRequest->headers->set('Accept-Language', $case['accept_lang']);

            // Note: This is simplified - in real testing you'd mock the request properly
            $detected = $case['expected']; // Placeholder for actual detection

            $status = $detected === $case['expected'] ? '' : '';
            $results[] = [
                $case['country'],
                $case['accept_lang'],
                $case['expected'],
                $detected,
                $status
            ];
        }

        $io->table(
            ['Country', 'Accept-Language', 'Expected', 'Detected', 'Status'],
            $results
        );

        return Command::SUCCESS;
    }
}

Bundle Development

Create Custom Bundle

<?php
// src/GeolocationBundle/GeolocationBundle.php

namespace App\GeolocationBundle;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class GeolocationBundle extends Bundle
{
    public function build(ContainerBuilder $container): void
    {
        parent::build($container);
    }
}

Bundle Configuration

<?php
// src/GeolocationBundle/DependencyInjection/Configuration.php

namespace App\GeolocationBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder(): TreeBuilder
    {
        $treeBuilder = new TreeBuilder('geolocation');

        $treeBuilder->getRootNode()
            ->children()
                ->arrayNode('country_to_language')
                    ->useAttributeAsKey('country')
                    ->arrayPrototype()
                        ->scalarPrototype()->end()
                    ->end()
                ->end()
                ->arrayNode('available_languages')
                    ->scalarPrototype()->end()
                ->end()
                ->scalarNode('default_language')->defaultValue('en')->end()
                ->scalarNode('cookie_name')->defaultValue('app_language')->end()
                ->integerNode('cookie_lifetime')->defaultValue(2592000)->end()
                ->booleanNode('enable_simulation')->defaultValue('%kernel.debug%')->end()
            ->end();

        return $treeBuilder;
    }
}

Testing Integration

Functional Tests

<?php
// tests/Controller/GeolocationControllerTest.php

namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class GeolocationControllerTest extends WebTestCase
{
    public function testHomepageWithGeolocation(): void
    {
        $client = static::createClient();

        // Simulate German visitor
        $client->getContainer()->get('test.private.request_stack')
               ->getCurrentRequest()
               ->server->set('HTTP_CF_IPCOUNTRY', 'DE');

        $crawler = $client->request('GET', '/');

        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Willkommen');
    }

    public function testApiGeolocationEndpoint(): void
    {
        $client = static::createClient();

        $client->request('GET', '/api/geolocation/detect');

        $this->assertResponseIsSuccessful();
        $this->assertJson($client->getResponse()->getContent());

        $data = json_decode($client->getResponse()->getContent(), true);
        $this->assertTrue($data['success']);
        $this->assertArrayHasKey('data', $data);
    }

    public function testLanguageSwitch(): void
    {
        $client = static::createClient();

        $client->request('GET', '/language/set/de');
        $this->assertResponseRedirects();

        // Check if cookie is set
        $cookies = $client->getResponse()->headers->getCookies();
        $languageCookie = null;

        foreach ($cookies as $cookie) {
            if ($cookie->getName() === 'app_language') {
                $languageCookie = $cookie;
                break;
            }
        }

        $this->assertNotNull($languageCookie);
        $this->assertEquals('de', $languageCookie->getValue());
    }
}

Next Steps


Previous: Language Negotiation | Next: API Reference

Clone this wiki locally