Skip to content

Configuration

Rumen Damyanov edited this page Jul 31, 2025 · 3 revisions

Configuration Guide

Advanced configuration options and customization patterns for the php-geolocation package.

Table of Contents

Basic Configuration

Simple Configuration

use Rumenx\Geolocation\Geolocation;

// Basic setup with default options
$geo = new Geolocation();

// With custom server data
$geo = new Geolocation($_SERVER);

// With language mapping
$countryMapping = [
    'US' => ['en'],
    'DE' => ['de'],
    'FR' => ['fr']
];
$geo = new Geolocation($_SERVER, $countryMapping);

// With custom cookie name
$geo = new Geolocation($_SERVER, $countryMapping, 'user_lang');

Configuration Class Pattern

class GeolocationConfig
{
    private array $countryToLanguage;
    private string $cookieName;
    private array $availableLanguages;
    private string $defaultLanguage;
    private array $options;

    public function __construct() {
        $this->setDefaults();
    }

    private function setDefaults(): void {
        $this->countryToLanguage = [
            'US' => ['en'], 'CA' => ['en', 'fr'], 'GB' => ['en'],
            'DE' => ['de'], 'AT' => ['de'], 'CH' => ['de', 'fr', 'it'],
            'FR' => ['fr'], 'BE' => ['fr', 'nl'], 'LU' => ['fr', 'de'],
            'ES' => ['es'], 'MX' => ['es'], 'AR' => ['es'],
            'BR' => ['pt'], 'PT' => ['pt'],
            'IT' => ['it'], 'NL' => ['nl'],
            'RU' => ['ru'], 'JP' => ['ja'], 'CN' => ['zh']
        ];

        $this->cookieName = 'app_language';
        $this->availableLanguages = ['en', 'fr', 'de', 'es'];
        $this->defaultLanguage = 'en';

        $this->options = [
            'enable_simulation' => true,
            'cache_duration' => 3600,
            'fallback_country' => 'US',
            'strict_mode' => false
        ];
    }

    public function setCountryMapping(array $mapping): self {
        $this->countryToLanguage = $mapping;
        return $this;
    }

    public function addCountryMapping(string $country, array $languages): self {
        $this->countryToLanguage[$country] = $languages;
        return $this;
    }

    public function setCookieName(string $name): self {
        $this->cookieName = $name;
        return $this;
    }

    public function setAvailableLanguages(array $languages): self {
        $this->availableLanguages = $languages;
        return $this;
    }

    public function setDefaultLanguage(string $language): self {
        $this->defaultLanguage = $language;
        return $this;
    }

    public function setOption(string $key, $value): self {
        $this->options[$key] = $value;
        return $this;
    }

    public function createGeolocation(array $server = []): Geolocation {
        $server = $server ?: $_SERVER;
        return new Geolocation($server, $this->countryToLanguage, $this->cookieName);
    }

    public function getCountryMapping(): array {
        return $this->countryToLanguage;
    }

    public function getCookieName(): string {
        return $this->cookieName;
    }

    public function getAvailableLanguages(): array {
        return $this->availableLanguages;
    }

    public function getDefaultLanguage(): string {
        return $this->defaultLanguage;
    }

    public function getOption(string $key, $default = null) {
        return $this->options[$key] ?? $default;
    }
}

// Usage
$config = new GeolocationConfig();
$config->setAvailableLanguages(['en', 'fr', 'de', 'es', 'pt'])
       ->setDefaultLanguage('en')
       ->setCookieName('site_language')
       ->addCountryMapping('BR', ['pt'])
       ->setOption('enable_simulation', $_ENV['APP_DEBUG'] ?? false);

$geo = $config->createGeolocation();

Country-Language Mapping

Comprehensive Mapping

function getComprehensiveCountryMapping(): array {
    return [
        // North America
        'US' => ['en'],
        'CA' => ['en', 'fr'],
        'MX' => ['es'],

        // South America
        'BR' => ['pt'],
        'AR' => ['es'],
        'CL' => ['es'],
        'CO' => ['es'],
        'PE' => ['es'],
        'VE' => ['es'],
        'UY' => ['es'],
        'PY' => ['es', 'gn'],
        'BO' => ['es', 'qu', 'ay'],
        'EC' => ['es'],
        'GY' => ['en'],
        'SR' => ['nl'],
        'GF' => ['fr'],

        // Europe - Western
        'GB' => ['en'],
        'IE' => ['en', 'ga'],
        'FR' => ['fr'],
        'DE' => ['de'],
        'AT' => ['de'],
        'CH' => ['de', 'fr', 'it', 'rm'],
        'LI' => ['de'],
        'LU' => ['fr', 'de', 'lb'],
        'BE' => ['fr', 'nl', 'de'],
        'NL' => ['nl'],
        'ES' => ['es', 'ca', 'gl', 'eu'],
        'PT' => ['pt'],
        'IT' => ['it'],
        'SM' => ['it'],
        'VA' => ['it', 'la'],
        'MT' => ['mt', 'en'],
        'MC' => ['fr'],
        'AD' => ['ca'],

        // Europe - Northern
        'DK' => ['da'],
        'SE' => ['sv'],
        'NO' => ['no', 'nb', 'nn'],
        'FI' => ['fi', 'sv'],
        'IS' => ['is'],

        // Europe - Eastern
        'PL' => ['pl'],
        'CZ' => ['cs'],
        'SK' => ['sk'],
        'HU' => ['hu'],
        'SI' => ['sl'],
        'HR' => ['hr'],
        'BA' => ['bs', 'hr', 'sr'],
        'ME' => ['sr', 'bs', 'hr'],
        'RS' => ['sr'],
        'MK' => ['mk'],
        'AL' => ['sq'],
        'BG' => ['bg'],
        'RO' => ['ro'],
        'MD' => ['ro', 'ru'],
        'UA' => ['uk', 'ru'],
        'BY' => ['be', 'ru'],
        'LT' => ['lt'],
        'LV' => ['lv'],
        'EE' => ['et'],
        'RU' => ['ru'],

        // Asia - East
        'CN' => ['zh'],
        'TW' => ['zh'],
        'HK' => ['zh', 'en'],
        'MO' => ['zh', 'pt'],
        'JP' => ['ja'],
        'KR' => ['ko'],
        'KP' => ['ko'],
        'MN' => ['mn'],

        // Asia - Southeast
        'TH' => ['th'],
        'VN' => ['vi'],
        'LA' => ['lo'],
        'KH' => ['km'],
        'MM' => ['my'],
        'MY' => ['ms', 'en', 'zh', 'ta'],
        'SG' => ['en', 'zh', 'ms', 'ta'],
        'BN' => ['ms'],
        'ID' => ['id'],
        'TL' => ['pt', 'tet'],
        'PH' => ['en', 'tl'],

        // Asia - South
        'IN' => ['hi', 'en'],
        'PK' => ['ur', 'en'],
        'BD' => ['bn'],
        'LK' => ['si', 'ta'],
        'MV' => ['dv'],
        'NP' => ['ne'],
        'BT' => ['dz'],
        'AF' => ['fa', 'ps'],

        // Asia - Central
        'KZ' => ['kk', 'ru'],
        'UZ' => ['uz', 'ru'],
        'TM' => ['tk', 'ru'],
        'KG' => ['ky', 'ru'],
        'TJ' => ['tg', 'ru'],

        // Asia - Western (Middle East)
        'TR' => ['tr'],
        'IR' => ['fa'],
        'IQ' => ['ar', 'ku'],
        'SY' => ['ar'],
        'LB' => ['ar', 'fr'],
        'JO' => ['ar'],
        'IL' => ['he', 'ar'],
        'PS' => ['ar'],
        'SA' => ['ar'],
        'YE' => ['ar'],
        'OM' => ['ar'],
        'AE' => ['ar'],
        'QA' => ['ar'],
        'BH' => ['ar'],
        'KW' => ['ar'],
        'GE' => ['ka'],
        'AM' => ['hy'],
        'AZ' => ['az'],
        'CY' => ['el', 'tr'],

        // Africa - North
        'EG' => ['ar'],
        'LY' => ['ar'],
        'TN' => ['ar', 'fr'],
        'DZ' => ['ar', 'fr'],
        'MA' => ['ar', 'fr'],
        'SD' => ['ar'],
        'SS' => ['en'],

        // Africa - West
        'NG' => ['en'],
        'GH' => ['en'],
        'CI' => ['fr'],
        'SN' => ['fr'],
        'ML' => ['fr'],
        'BF' => ['fr'],
        'NE' => ['fr'],
        'GN' => ['fr'],
        'SL' => ['en'],
        'LR' => ['en'],
        'GM' => ['en'],
        'GW' => ['pt'],
        'CV' => ['pt'],

        // Africa - East
        'ET' => ['am'],
        'KE' => ['sw', 'en'],
        'UG' => ['en'],
        'TZ' => ['sw', 'en'],
        'RW' => ['rw', 'en', 'fr'],
        'BI' => ['rn', 'fr'],
        'SO' => ['so', 'ar'],
        'DJ' => ['fr', 'ar'],
        'ER' => ['ti', 'ar'],

        // Africa - Central
        'CD' => ['fr'],
        'CG' => ['fr'],
        'CF' => ['fr'],
        'CM' => ['fr', 'en'],
        'TD' => ['fr', 'ar'],
        'GA' => ['fr'],
        'GQ' => ['es', 'fr'],
        'ST' => ['pt'],
        'AO' => ['pt'],

        // Africa - Southern
        'ZA' => ['en', 'af', 'zu'],
        'ZW' => ['en'],
        'BW' => ['en'],
        'NA' => ['en', 'af'],
        'ZM' => ['en'],
        'MW' => ['en'],
        'MZ' => ['pt'],
        'MG' => ['mg', 'fr'],
        'MU' => ['en', 'fr'],
        'SC' => ['en', 'fr'],
        'KM' => ['ar', 'fr'],
        'LS' => ['en'],
        'SZ' => ['en'],

        // Oceania
        'AU' => ['en'],
        'NZ' => ['en', 'mi'],
        'PG' => ['en', 'tpi'],
        'FJ' => ['en', 'fj'],
        'VU' => ['bi', 'en', 'fr'],
        'SB' => ['en'],
        'NC' => ['fr'],
        'PF' => ['fr'],
        'WS' => ['sm', 'en'],
        'TO' => ['to', 'en'],
        'KI' => ['en'],
        'TV' => ['en'],
        'NR' => ['en'],
        'PW' => ['en'],
        'FM' => ['en'],
        'MH' => ['en'],
    ];
}

Custom Regional Mappings

class RegionalMapping
{
    private array $regions;

    public function __construct() {
        $this->regions = [
            'EU' => [
                'countries' => ['DE', 'FR', 'ES', 'IT', 'NL', 'BE', 'AT', 'PL'],
                'languages' => ['en', 'de', 'fr', 'es', 'it'],
                'default' => 'en'
            ],
            'LATAM' => [
                'countries' => ['MX', 'AR', 'CL', 'CO', 'PE', 'VE'],
                'languages' => ['es', 'pt', 'en'],
                'default' => 'es'
            ],
            'ASIA' => [
                'countries' => ['JP', 'KR', 'CN', 'TH', 'VN', 'ID'],
                'languages' => ['en', 'ja', 'ko', 'zh'],
                'default' => 'en'
            ],
            'MENA' => [
                'countries' => ['SA', 'AE', 'EG', 'MA', 'TR'],
                'languages' => ['ar', 'en', 'tr'],
                'default' => 'en'
            ]
        ];
    }

    public function getRegionForCountry(string $country): ?string {
        foreach ($this->regions as $region => $data) {
            if (in_array($country, $data['countries'])) {
                return $region;
            }
        }
        return null;
    }

    public function getLanguagesForRegion(string $region): array {
        return $this->regions[$region]['languages'] ?? [];
    }

    public function getDefaultLanguageForRegion(string $region): string {
        return $this->regions[$region]['default'] ?? 'en';
    }

    public function createMappingForRegions(array $regions): array {
        $mapping = [];
        $comprehensive = getComprehensiveCountryMapping();

        foreach ($regions as $region) {
            if (!isset($this->regions[$region])) continue;

            $regionData = $this->regions[$region];
            foreach ($regionData['countries'] as $country) {
                // Use regional languages if country not in comprehensive mapping
                $mapping[$country] = $comprehensive[$country] ?? $regionData['languages'];
            }
        }

        return $mapping;
    }
}

// Usage
$regionalMapping = new RegionalMapping();

// Create mapping for specific regions
$euMapping = $regionalMapping->createMappingForRegions(['EU']);
$globalMapping = $regionalMapping->createMappingForRegions(['EU', 'LATAM', 'ASIA']);

$config = new GeolocationConfig();
$config->setCountryMapping($globalMapping);

Cookie Configuration

Advanced Cookie Management

class CookieConfig
{
    private string $name;
    private int $lifetime;
    private string $path;
    private ?string $domain;
    private bool $secure;
    private bool $httpOnly;
    private string $sameSite;

    public function __construct(array $options = []) {
        $this->name = $options['name'] ?? 'app_language';
        $this->lifetime = $options['lifetime'] ?? 2592000; // 30 days
        $this->path = $options['path'] ?? '/';
        $this->domain = $options['domain'] ?? null;
        $this->secure = $options['secure'] ?? false;
        $this->httpOnly = $options['httpOnly'] ?? true;
        $this->sameSite = $options['sameSite'] ?? 'Lax';
    }

    public function setCookie(string $value): bool {
        return setcookie(
            $this->name,
            $value,
            [
                'expires' => time() + $this->lifetime,
                'path' => $this->path,
                'domain' => $this->domain,
                'secure' => $this->secure,
                'httponly' => $this->httpOnly,
                'samesite' => $this->sameSite
            ]
        );
    }

    public function getCookie(): ?string {
        return $_COOKIE[$this->name] ?? null;
    }

    public function deleteCookie(): bool {
        return setcookie(
            $this->name,
            '',
            [
                'expires' => time() - 3600,
                'path' => $this->path,
                'domain' => $this->domain,
                'secure' => $this->secure,
                'httponly' => $this->httpOnly,
                'samesite' => $this->sameSite
            ]
        );
    }

    public function getName(): string {
        return $this->name;
    }

    public static function create(string $name, array $options = []): self {
        return new self(array_merge(['name' => $name], $options));
    }
}

// Usage
$cookieConfig = CookieConfig::create('user_language', [
    'lifetime' => 86400 * 90, // 90 days
    'secure' => $_SERVER['HTTPS'] ?? false,
    'domain' => '.example.com' // Set for subdomain sharing
]);

// Multiple cookie configurations
$mainCookie = CookieConfig::create('main_lang');
$adminCookie = CookieConfig::create('admin_lang', ['path' => '/admin']);
$apiCookie = CookieConfig::create('api_lang', ['httpOnly' => false]); // Accessible via JS

Encrypted Cookie Storage

class EncryptedCookieConfig extends CookieConfig
{
    private string $encryptionKey;

    public function __construct(array $options = []) {
        parent::__construct($options);
        $this->encryptionKey = $options['encryption_key'] ??
                              $_ENV['APP_KEY'] ??
                              'default-key-change-in-production';
    }

    public function setCookie(string $value): bool {
        $encrypted = $this->encrypt($value);
        return parent::setCookie($encrypted);
    }

    public function getCookie(): ?string {
        $encrypted = parent::getCookie();
        return $encrypted ? $this->decrypt($encrypted) : null;
    }

    private function encrypt(string $data): string {
        $iv = random_bytes(16);
        $encrypted = openssl_encrypt($data, 'AES-256-CBC', $this->encryptionKey, 0, $iv);
        return base64_encode($iv . $encrypted);
    }

    private function decrypt(string $data): ?string {
        $data = base64_decode($data);
        if ($data === false || strlen($data) < 16) {
            return null;
        }

        $iv = substr($data, 0, 16);
        $encrypted = substr($data, 16);

        $decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $this->encryptionKey, 0, $iv);
        return $decrypted !== false ? $decrypted : null;
    }
}

// Usage
$encryptedCookie = new EncryptedCookieConfig([
    'name' => 'secure_language',
    'encryption_key' => $_ENV['COOKIE_ENCRYPTION_KEY']
]);

Environment-Based Configuration

Environment Detection

class EnvironmentConfig
{
    private string $environment;
    private array $configs;

    public function __construct() {
        $this->environment = $this->detectEnvironment();
        $this->configs = $this->loadConfigs();
    }

    private function detectEnvironment(): string {
        // Check environment variable
        if ($env = $_ENV['APP_ENV'] ?? $_ENV['ENVIRONMENT'] ?? null) {
            return $env;
        }

        // Check hostname patterns
        $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '';

        if (preg_match('/localhost|127\.0\.0\.1|\.local$|\.dev$/', $host)) {
            return 'development';
        } elseif (preg_match('/staging|test/', $host)) {
            return 'staging';
        } else {
            return 'production';
        }
    }

    private function loadConfigs(): array {
        return [
            'development' => [
                'enable_simulation' => true,
                'debug_headers' => true,
                'cache_duration' => 0,
                'cookie_secure' => false,
                'available_languages' => ['en', 'fr', 'de', 'es', 'pt'],
                'fallback_country' => 'US',
                'strict_validation' => false
            ],
            'staging' => [
                'enable_simulation' => true,
                'debug_headers' => true,
                'cache_duration' => 300, // 5 minutes
                'cookie_secure' => true,
                'available_languages' => ['en', 'fr', 'de', 'es'],
                'fallback_country' => 'US',
                'strict_validation' => true
            ],
            'production' => [
                'enable_simulation' => false,
                'debug_headers' => false,
                'cache_duration' => 3600, // 1 hour
                'cookie_secure' => true,
                'available_languages' => ['en', 'fr', 'de'],
                'fallback_country' => 'US',
                'strict_validation' => true
            ]
        ];
    }

    public function get(string $key, $default = null) {
        return $this->configs[$this->environment][$key] ?? $default;
    }

    public function getEnvironment(): string {
        return $this->environment;
    }

    public function isProduction(): bool {
        return $this->environment === 'production';
    }

    public function isDevelopment(): bool {
        return $this->environment === 'development';
    }

    public function createGeolocationConfig(): GeolocationConfig {
        $config = new GeolocationConfig();

        $config->setAvailableLanguages($this->get('available_languages'))
               ->setOption('enable_simulation', $this->get('enable_simulation'))
               ->setOption('cache_duration', $this->get('cache_duration'))
               ->setOption('fallback_country', $this->get('fallback_country'));

        return $config;
    }
}

// Usage
$envConfig = new EnvironmentConfig();
$geoConfig = $envConfig->createGeolocationConfig();

if ($envConfig->isDevelopment()) {
    // Development-specific settings
    $geoConfig->addCountryMapping('TEST', ['en']);
}

$geo = $geoConfig->createGeolocation();

Configuration Files

class ConfigurationLoader
{
    private array $config;
    private string $configPath;

    public function __construct(string $configPath = null) {
        $this->configPath = $configPath ?: __DIR__ . '/config';
        $this->config = $this->loadConfiguration();
    }

    private function loadConfiguration(): array {
        $baseConfig = $this->loadFile('geolocation.php');
        $envConfig = $this->loadFile('geolocation.' . $this->getEnvironment() . '.php');

        return array_merge($baseConfig, $envConfig);
    }

    private function loadFile(string $filename): array {
        $filepath = $this->configPath . '/' . $filename;

        if (file_exists($filepath)) {
            return include $filepath;
        }

        return [];
    }

    private function getEnvironment(): string {
        return $_ENV['APP_ENV'] ?? 'production';
    }

    public function get(string $key, $default = null) {
        return $this->getNestedValue($this->config, $key, $default);
    }

    private function getNestedValue(array $array, string $key, $default = null) {
        if (strpos($key, '.') === false) {
            return $array[$key] ?? $default;
        }

        $keys = explode('.', $key);
        $value = $array;

        foreach ($keys as $k) {
            if (!is_array($value) || !isset($value[$k])) {
                return $default;
            }
            $value = $value[$k];
        }

        return $value;
    }

    public function createGeolocation(): Geolocation {
        $countryMapping = $this->get('country_to_language', []);
        $cookieName = $this->get('cookie.name', 'language');

        return new Geolocation($_SERVER, $countryMapping, $cookieName);
    }
}

// Configuration files:

// config/geolocation.php (base configuration)
/*
<?php
return [
    'country_to_language' => [
        'US' => ['en'], 'CA' => ['en', 'fr'], 'GB' => ['en'],
        'DE' => ['de'], 'FR' => ['fr'], 'ES' => ['es'],
    ],
    'available_languages' => ['en', 'fr', 'de', 'es'],
    'default_language' => 'en',
    'cookie' => [
        'name' => 'app_language',
        'lifetime' => 2592000, // 30 days
        'secure' => false,
        'httponly' => true
    ],
    'simulation' => [
        'enabled' => false,
        'default_country' => 'US'
    ],
    'cache' => [
        'enabled' => true,
        'duration' => 3600
    ]
];
*/

// config/geolocation.development.php (development overrides)
/*
<?php
return [
    'simulation' => [
        'enabled' => true,
        'default_country' => $_ENV['DEV_COUNTRY'] ?? 'US'
    ],
    'cookie' => [
        'secure' => false
    ],
    'cache' => [
        'duration' => 60 // Short cache in development
    ]
];
*/

// Usage
$configLoader = new ConfigurationLoader();
$geo = $configLoader->createGeolocation();

$enableSimulation = $configLoader->get('simulation.enabled');
$cacheEnabled = $configLoader->get('cache.enabled');

Framework-Specific Configuration

Laravel Configuration

// config/geolocation.php for Laravel
<?php

return [
    /*
    |--------------------------------------------------------------------------
    | Country to Language Mapping
    |--------------------------------------------------------------------------
    */
    'country_to_language' => [
        'US' => ['en'], 'CA' => ['en', 'fr'], 'GB' => ['en'],
        'DE' => ['de'], 'AT' => ['de'], 'CH' => ['de', 'fr', 'it'],
        'FR' => ['fr'], 'BE' => ['fr', 'nl'], 'ES' => ['es'],
        'IT' => ['it'], 'NL' => ['nl'], 'PT' => ['pt'], 'BR' => ['pt'],
        'RU' => ['ru'], 'JP' => ['ja'], 'CN' => ['zh'],
    ],

    /*
    |--------------------------------------------------------------------------
    | Available Languages
    |--------------------------------------------------------------------------
    */
    'available_languages' => explode(',', env('GEOLOCATION_LANGUAGES', 'en,fr,de,es')),

    /*
    |--------------------------------------------------------------------------
    | Default Settings
    |--------------------------------------------------------------------------
    */
    'default_language' => env('GEOLOCATION_DEFAULT_LANGUAGE', 'en'),
    'cookie_name' => env('GEOLOCATION_COOKIE', 'app_language'),
    'cookie_lifetime' => env('GEOLOCATION_COOKIE_LIFETIME', 2592000),

    /*
    |--------------------------------------------------------------------------
    | Simulation Settings
    |--------------------------------------------------------------------------
    */
    'simulation' => [
        'enabled' => env('GEOLOCATION_SIMULATION', env('APP_DEBUG', false)),
        'default_country' => env('GEOLOCATION_SIMULATE_COUNTRY'),
        'countries' => ['US', 'CA', 'GB', 'DE', 'FR', 'ES', 'JP', 'BR'],
    ],

    /*
    |--------------------------------------------------------------------------
    | Cache Settings
    |--------------------------------------------------------------------------
    */
    'cache' => [
        'enabled' => env('GEOLOCATION_CACHE', true),
        'duration' => env('GEOLOCATION_CACHE_DURATION', 3600),
        'key_prefix' => 'geo:',
    ],

    /*
    |--------------------------------------------------------------------------
    | Security Settings
    |--------------------------------------------------------------------------
    */
    'security' => [
        'encrypt_cookies' => env('GEOLOCATION_ENCRYPT_COOKIES', false),
        'validate_headers' => env('GEOLOCATION_VALIDATE_HEADERS', true),
        'rate_limit' => env('GEOLOCATION_RATE_LIMIT', 100), // per minute
    ],
];

Symfony Configuration

# config/packages/geolocation.yaml
geolocation:
    country_to_language:
        US: [en]
        CA: [en, fr]
        GB: [en]
        DE: [de]
        AT: [de]
        CH: [de, fr, it]
        FR: [fr]
        BE: [fr, nl]
        ES: [es]
        IT: [it]
        NL: [nl]
        PT: [pt]
        BR: [pt]
        RU: [ru]
        JP: [ja]
        CN: [zh]

    available_languages: ['en', 'fr', 'de', 'es']
    default_language: en

    cookie:
        name: app_language
        lifetime: 2592000  # 30 days
        secure: '%kernel.environment% == "prod"'
        httponly: true
        samesite: Lax

    simulation:
        enabled: '%kernel.debug%'
        default_country: '%env(GEOLOCATION_SIMULATE_COUNTRY)%'

    cache:
        enabled: true
        duration: 3600
        service: cache.app

Advanced Patterns

Configuration Builder

class GeolocationConfigBuilder
{
    private GeolocationConfig $config;

    public function __construct() {
        $this->config = new GeolocationConfig();
    }

    public function forRegion(string $region): self {
        $regionalMapping = new RegionalMapping();
        $mapping = $regionalMapping->createMappingForRegions([$region]);
        $languages = $regionalMapping->getLanguagesForRegion($region);
        $defaultLang = $regionalMapping->getDefaultLanguageForRegion($region);

        $this->config->setCountryMapping($mapping)
                     ->setAvailableLanguages($languages)
                     ->setDefaultLanguage($defaultLang);

        return $this;
    }

    public function forEnvironment(string $env): self {
        $envConfig = new EnvironmentConfig();

        switch ($env) {
            case 'development':
                $this->config->setOption('enable_simulation', true)
                             ->setOption('debug_headers', true)
                             ->setOption('cache_duration', 0);
                break;
            case 'production':
                $this->config->setOption('enable_simulation', false)
                             ->setOption('debug_headers', false)
                             ->setOption('cache_duration', 3600);
                break;
        }

        return $this;
    }

    public function withLanguages(array $languages): self {
        $this->config->setAvailableLanguages($languages);
        return $this;
    }

    public function withSecureCookies(bool $secure = true): self {
        $this->config->setOption('secure_cookies', $secure);
        return $this;
    }

    public function withCaching(int $duration = 3600): self {
        $this->config->setOption('cache_enabled', true)
                     ->setOption('cache_duration', $duration);
        return $this;
    }

    public function withFallback(string $country, string $language): self {
        $this->config->setOption('fallback_country', $country)
                     ->setDefaultLanguage($language);
        return $this;
    }

    public function build(): GeolocationConfig {
        return $this->config;
    }
}

// Usage examples
$config = (new GeolocationConfigBuilder())
    ->forRegion('EU')
    ->forEnvironment('production')
    ->withLanguages(['en', 'de', 'fr'])
    ->withSecureCookies()
    ->withCaching(7200)
    ->build();

$geo = $config->createGeolocation();

// Or for development
$devConfig = (new GeolocationConfigBuilder())
    ->forEnvironment('development')
    ->withLanguages(['en', 'fr', 'de', 'es', 'pt'])
    ->withFallback('US', 'en')
    ->build();

Dynamic Configuration

class DynamicGeolocationConfig
{
    private array $rules;

    public function __construct() {
        $this->rules = [];
    }

    public function addRule(callable $condition, array $config): self {
        $this->rules[] = [
            'condition' => $condition,
            'config' => $config
        ];
        return $this;
    }

    public function forUserAgent(string $pattern, array $config): self {
        return $this->addRule(
            fn() => preg_match($pattern, $_SERVER['HTTP_USER_AGENT'] ?? ''),
            $config
        );
    }

    public function forDomain(string $domain, array $config): self {
        return $this->addRule(
            fn() => strpos($_SERVER['HTTP_HOST'] ?? '', $domain) !== false,
            $config
        );
    }

    public function forCountry(string $country, array $config): self {
        return $this->addRule(
            function() use ($country) {
                $geo = new Geolocation($_SERVER);
                return $geo->getCountryCode() === $country;
            },
            $config
        );
    }

    public function forTime(string $timezone, int $startHour, int $endHour, array $config): self {
        return $this->addRule(
            function() use ($timezone, $startHour, $endHour) {
                $tz = new DateTimeZone($timezone);
                $time = new DateTime('now', $tz);
                $hour = (int)$time->format('H');
                return $hour >= $startHour && $hour <= $endHour;
            },
            $config
        );
    }

    public function resolve(): GeolocationConfig {
        $config = new GeolocationConfig();

        foreach ($this->rules as $rule) {
            if (($rule['condition'])()) {
                foreach ($rule['config'] as $key => $value) {
                    if (method_exists($config, 'set' . ucfirst($key))) {
                        $config->{'set' . ucfirst($key)}($value);
                    } else {
                        $config->setOption($key, $value);
                    }
                }
            }
        }

        return $config;
    }
}

// Usage
$dynamicConfig = new DynamicGeolocationConfig();

$dynamicConfig
    ->forDomain('admin.example.com', [
        'availableLanguages' => ['en'],
        'cookieName' => 'admin_language'
    ])
    ->forUserAgent('/Mobile/', [
        'availableLanguages' => ['en', 'es'], // Limited languages for mobile
        'enable_simulation' => false
    ])
    ->forCountry('CN', [
        'availableLanguages' => ['zh', 'en'],
        'defaultLanguage' => 'zh'
    ])
    ->forTime('America/New_York', 9, 17, [
        'cache_duration' => 1800 // Shorter cache during business hours
    ]);

$config = $dynamicConfig->resolve();
$geo = $config->createGeolocation();

Performance Optimization

Caching Configuration

class CachedGeolocationConfig
{
    private $cache;
    private int $cacheDuration;

    public function __construct($cache, int $duration = 3600) {
        $this->cache = $cache;
        $this->cacheDuration = $duration;
    }

    public function getCachedGeoInfo(string $cacheKey, callable $resolver): array {
        // Try to get from cache first
        $cached = $this->cache->get($cacheKey);

        if ($cached !== null) {
            return $cached;
        }

        // Resolve and cache
        $geoInfo = $resolver();
        $this->cache->set($cacheKey, $geoInfo, $this->cacheDuration);

        return $geoInfo;
    }

    public function createCachedGeolocation(): Geolocation {
        $cacheKey = $this->generateCacheKey();

        return $this->getCachedGeoInfo($cacheKey, function() {
            return new Geolocation($_SERVER);
        });
    }

    private function generateCacheKey(): string {
        $factors = [
            $_SERVER['HTTP_CF_IPCOUNTRY'] ?? '',
            $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '',
            $_SERVER['HTTP_USER_AGENT'] ?? '',
            $_SERVER['REMOTE_ADDR'] ?? ''
        ];

        return 'geo:' . hash('crc32', implode('|', $factors));
    }

    public function warmCache(array $countries): void {
        foreach ($countries as $country) {
            $geo = Geolocation::simulate($country);
            $cacheKey = 'geo:simulated:' . $country;
            $this->cache->set($cacheKey, $geo->getGeoInfo(), $this->cacheDuration);
        }
    }
}

// Usage with different cache backends

// With APCu
class ApcuCache {
    public function get(string $key) {
        return apcu_fetch($key) ?: null;
    }

    public function set(string $key, $value, int $duration): void {
        apcu_store($key, $value, $duration);
    }
}

// With Redis
class RedisCache {
    private $redis;

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

    public function get(string $key) {
        $value = $this->redis->get($key);
        return $value ? unserialize($value) : null;
    }

    public function set(string $key, $value, int $duration): void {
        $this->redis->setex($key, $duration, serialize($value));
    }
}

// Usage
$cache = new ApcuCache(); // or new RedisCache($redis)
$cachedConfig = new CachedGeolocationConfig($cache, 1800);

// Warm cache for common countries
$cachedConfig->warmCache(['US', 'GB', 'DE', 'FR', 'ES', 'JP']);

Security Considerations

Input Validation

class SecureGeolocationConfig
{
    private array $allowedCountries;
    private array $allowedLanguages;
    private bool $strictValidation;

    public function __construct(array $allowedCountries = [], bool $strict = true) {
        $this->allowedCountries = $allowedCountries;
        $this->allowedLanguages = $this->getValidLanguageCodes();
        $this->strictValidation = $strict;
    }

    public function validateAndCreateGeolocation(array $server): ?Geolocation {
        if (!$this->validateServerData($server)) {
            return null;
        }

        $cleanServer = $this->sanitizeServerData($server);
        return new Geolocation($cleanServer);
    }

    private function validateServerData(array $server): bool {
        // Validate country code
        if (isset($server['HTTP_CF_IPCOUNTRY'])) {
            if (!$this->isValidCountryCode($server['HTTP_CF_IPCOUNTRY'])) {
                return false;
            }
        }

        // Validate IP address
        if (isset($server['REMOTE_ADDR'])) {
            if (!filter_var($server['REMOTE_ADDR'], FILTER_VALIDATE_IP)) {
                return false;
            }
        }

        // Validate User-Agent length
        if (isset($server['HTTP_USER_AGENT'])) {
            if (strlen($server['HTTP_USER_AGENT']) > 1000) {
                return false;
            }
        }

        return true;
    }

    private function sanitizeServerData(array $server): array {
        $clean = [];

        // Sanitize country code
        if (isset($server['HTTP_CF_IPCOUNTRY'])) {
            $country = strtoupper(trim($server['HTTP_CF_IPCOUNTRY']));
            if ($this->isValidCountryCode($country)) {
                $clean['HTTP_CF_IPCOUNTRY'] = $country;
            }
        }

        // Sanitize and validate IP
        if (isset($server['REMOTE_ADDR'])) {
            $ip = filter_var($server['REMOTE_ADDR'], FILTER_VALIDATE_IP);
            if ($ip) {
                $clean['REMOTE_ADDR'] = $ip;
            }
        }

        // Sanitize User-Agent
        if (isset($server['HTTP_USER_AGENT'])) {
            $userAgent = strip_tags(trim($server['HTTP_USER_AGENT']));
            $clean['HTTP_USER_AGENT'] = substr($userAgent, 0, 500);
        }

        // Sanitize Accept-Language
        if (isset($server['HTTP_ACCEPT_LANGUAGE'])) {
            $acceptLang = $this->sanitizeAcceptLanguage($server['HTTP_ACCEPT_LANGUAGE']);
            if ($acceptLang) {
                $clean['HTTP_ACCEPT_LANGUAGE'] = $acceptLang;
            }
        }

        return $clean;
    }

    private function isValidCountryCode(string $code): bool {
        if (!preg_match('/^[A-Z]{2}$/', $code)) {
            return false;
        }

        if (!empty($this->allowedCountries)) {
            return in_array($code, $this->allowedCountries);
        }

        return true;
    }

    private function sanitizeAcceptLanguage(string $acceptLanguage): ?string {
        // Remove any suspicious characters
        $clean = preg_replace('/[^a-zA-Z0-9\-,;=\.\s]/', '', $acceptLanguage);

        // Validate format
        if (!preg_match('/^[a-zA-Z\-,;=\.\s\d]+$/', $clean)) {
            return null;
        }

        return substr($clean, 0, 200);
    }

    private function getValidLanguageCodes(): array {
        return [
            'en', 'fr', 'de', 'es', 'it', 'pt', 'ru', 'ja', 'zh', 'ko',
            'ar', 'hi', 'th', 'vi', 'nl', 'sv', 'da', 'no', 'fi', 'pl',
            'tr', 'he', 'id', 'ms', 'tl', 'sw', 'am', 'bn', 'gu', 'kn',
            'ml', 'ta', 'te', 'ur', 'fa', 'ku', 'az', 'ka', 'hy', 'be',
            'bg', 'hr', 'cs', 'et', 'lv', 'lt', 'hu', 'mt', 'ro', 'sk',
            'sl', 'sq', 'sr', 'uk', 'mk', 'is', 'ga', 'cy', 'eu', 'ca',
            'gl', 'lb', 'mi', 'sm', 'to', 'fj', 'mg', 'ny', 'sn', 'so',
            'rw', 'lg', 'wo', 'ig', 'yo', 'ha', 'zu', 'af', 'st', 'tn',
            'ts', 've', 'xh', 'ss', 'nr', 'nso', 'hy'
        ];
    }
}

// Usage
$secureConfig = new SecureGeolocationConfig(
    ['US', 'CA', 'GB', 'DE', 'FR', 'ES'], // Allowed countries
    true // Strict validation
);

$geo = $secureConfig->validateAndCreateGeolocation($_SERVER);

if (!$geo) {
    // Handle validation failure
    error_log('Geolocation validation failed for: ' . $_SERVER['REMOTE_ADDR']);
    $geo = Geolocation::simulate('US'); // Safe fallback
}

Next Steps


Previous: Client Detection | Next: Troubleshooting

Clone this wiki locally