Skip to content

Production

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

Production Best Practices

Comprehensive guide for deploying and maintaining php-geolocation in production environments with performance, security, and monitoring considerations.

Table of Contents

Production Deployment Checklist

Pre-Deployment Verification

#!/bin/bash
# production-deployment-check.sh

echo "=== php-geolocation Production Deployment Checklist ==="
echo

# Check PHP version
PHP_VERSION=$(php -r "echo PHP_VERSION;")
echo "✓ PHP Version: $PHP_VERSION"
if [[ $(echo "$PHP_VERSION" | cut -d. -f1) -lt 8 ]] || [[ $(echo "$PHP_VERSION" | cut -d. -f2) -lt 3 ]]; then
    echo "❌ ERROR: PHP 8.3+ required"
    exit 1
fi

# Check Composer dependencies
echo "✓ Checking Composer dependencies..."
composer validate --strict
composer install --no-dev --optimize-autoloader

# Check CloudFlare configuration
echo "✓ Testing CloudFlare integration..."
curl -sI https://yourdomain.com | grep -q "CF-Ray" && echo "✓ CloudFlare active" || echo "❌ CloudFlare not detected"

# Run tests
echo "✓ Running production test suite..."
./vendor/bin/pest --testsuite=production --coverage

# Check configuration
echo "✓ Validating configuration..."
php -f production-config-check.php

# Security scan
echo "✓ Running security checks..."
composer audit

echo
echo "=== Deployment Ready ✓ ==="

Environment Configuration

<?php
// config/production.php
return [
    'geolocation' => [
        // Performance settings
        'cache_enabled' => true,
        'cache_ttl' => 3600, // 1 hour
        'cache_driver' => 'redis',

        // Security settings
        'validate_headers' => true,
        'trusted_proxies' => [
            // CloudFlare IP ranges
            '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22',
            // Add your load balancer IPs
            '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'
        ],

        // Fallback settings
        'fallback_country' => 'US',
        'fallback_language' => 'en',
        'simulation_enabled' => false, // Disable in production

        // Monitoring
        'logging_enabled' => true,
        'log_level' => 'warning',
        'metrics_enabled' => true,

        // Rate limiting
        'rate_limit_enabled' => true,
        'rate_limit_requests' => 1000,
        'rate_limit_window' => 3600
    ]
];

Production Container Setup

# Dockerfile.production
FROM php:8.3-fpm-alpine

# Install system dependencies
RUN apk add --no-cache \
    nginx \
    redis \
    supervisor \
    curl \
    && docker-php-ext-install \
    opcache \
    pdo_mysql

# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Configure PHP for production
COPY docker/php/production.ini /usr/local/etc/php/conf.d/production.ini
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf

# Copy application
WORKDIR /var/www/html
COPY . .

# Install dependencies (production optimized)
RUN composer install --no-dev --optimize-autoloader --no-scripts \
    && composer dump-autoload --optimize --classmap-authoritative

# Set permissions
RUN chown -R www-data:www-data /var/www/html \
    && chmod -R 755 /var/www/html/storage

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost/health || exit 1

EXPOSE 80

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
; docker/php/production.ini
[PHP]
; Performance
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
opcache.save_comments=0
opcache.fast_shutdown=1

; Security
expose_php=Off
display_errors=Off
log_errors=On
error_log=/var/log/php/error.log

; Memory and limits
memory_limit=256M
max_execution_time=30
max_input_time=30
post_max_size=32M
upload_max_filesize=32M

; Session security
session.cookie_httponly=1
session.cookie_secure=1
session.use_strict_mode=1

Performance Optimization

Optimized Geolocation Class

<?php
// src/ProductionGeolocation.php
namespace Rumenx\Geolocation;

use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;

class ProductionGeolocation extends Geolocation
{
    private CacheItemPoolInterface $cache;
    private LoggerInterface $logger;
    private array $config;
    private static array $staticCache = [];

    public function __construct(
        CacheItemPoolInterface $cache,
        LoggerInterface $logger,
        array $config = [],
        array $server = null,
        array $countryToLanguage = [],
        string $cookieName = 'language'
    ) {
        $this->cache = $cache;
        $this->logger = $logger;
        $this->config = array_merge($this->getDefaultConfig(), $config);

        parent::__construct($server, $countryToLanguage, $cookieName);
    }

    public function getCountryCode(): string
    {
        $cacheKey = 'geo_country_' . $this->getClientHash();

        // Check static cache first (fastest)
        if (isset(self::$staticCache[$cacheKey])) {
            return self::$staticCache[$cacheKey];
        }

        // Check distributed cache
        if ($this->config['cache_enabled']) {
            $cached = $this->cache->getItem($cacheKey);
            if ($cached->isHit()) {
                $country = $cached->get();
                self::$staticCache[$cacheKey] = $country;
                return $country;
            }
        }

        // Get country with error handling
        try {
            $country = parent::getCountryCode();

            // Cache the result
            if ($this->config['cache_enabled']) {
                $cached->set($country);
                $cached->expiresAfter($this->config['cache_ttl']);
                $this->cache->save($cached);
            }

            self::$staticCache[$cacheKey] = $country;
            return $country;

        } catch (\Exception $e) {
            $this->logger->warning('Geolocation detection failed', [
                'error' => $e->getMessage(),
                'ip' => $this->getClientIP(),
                'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null
            ]);

            return $this->config['fallback_country'];
        }
    }

    public function getLanguage(): string
    {
        $cacheKey = 'geo_language_' . $this->getClientHash();

        if (isset(self::$staticCache[$cacheKey])) {
            return self::$staticCache[$cacheKey];
        }

        if ($this->config['cache_enabled']) {
            $cached = $this->cache->getItem($cacheKey);
            if ($cached->isHit()) {
                $language = $cached->get();
                self::$staticCache[$cacheKey] = $language;
                return $language;
            }
        }

        try {
            $language = parent::getLanguage();

            if ($this->config['cache_enabled']) {
                $cached->set($language);
                $cached->expiresAfter($this->config['cache_ttl']);
                $this->cache->save($cached);
            }

            self::$staticCache[$cacheKey] = $language;
            return $language;

        } catch (\Exception $e) {
            $this->logger->warning('Language detection failed', [
                'error' => $e->getMessage(),
                'country' => $this->getCountryCode()
            ]);

            return $this->config['fallback_language'];
        }
    }

    private function getClientHash(): string
    {
        $data = [
            $this->getClientIP(),
            $_SERVER['HTTP_CF_IPCOUNTRY'] ?? '',
            $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''
        ];

        return md5(implode('|', $data));
    }

    private function getClientIP(): string
    {
        $headers = [
            'HTTP_CF_CONNECTING_IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_REAL_IP',
            'REMOTE_ADDR'
        ];

        foreach ($headers as $header) {
            if (!empty($_SERVER[$header])) {
                $ip = trim(explode(',', $_SERVER[$header])[0]);
                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                    return $ip;
                }
            }
        }

        return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
    }

    private function getDefaultConfig(): array
    {
        return [
            'cache_enabled' => true,
            'cache_ttl' => 3600,
            'fallback_country' => 'US',
            'fallback_language' => 'en'
        ];
    }
}

Performance Monitoring

<?php
// src/GeolocationProfiler.php
class GeolocationProfiler
{
    private array $metrics = [];
    private float $startTime;

    public function start(string $operation): void
    {
        $this->startTime = microtime(true);
        $this->metrics[$operation] = ['start' => $this->startTime];
    }

    public function end(string $operation): float
    {
        $endTime = microtime(true);
        $duration = $endTime - $this->startTime;

        $this->metrics[$operation]['end'] = $endTime;
        $this->metrics[$operation]['duration'] = $duration;

        return $duration;
    }

    public function getMetrics(): array
    {
        return $this->metrics;
    }

    public function logSlowOperations(LoggerInterface $logger, float $threshold = 0.1): void
    {
        foreach ($this->metrics as $operation => $data) {
            if (isset($data['duration']) && $data['duration'] > $threshold) {
                $logger->warning('Slow geolocation operation', [
                    'operation' => $operation,
                    'duration' => $data['duration'],
                    'threshold' => $threshold
                ]);
            }
        }
    }
}

// Usage in production
$profiler = new GeolocationProfiler();

$profiler->start('country_detection');
$country = $geo->getCountryCode();
$countryTime = $profiler->end('country_detection');

$profiler->start('language_detection');
$language = $geo->getLanguage();
$languageTime = $profiler->end('language_detection');

// Log slow operations
$profiler->logSlowOperations($logger, 0.05); // 50ms threshold

Security Considerations

Security-Hardened Implementation

<?php
// src/SecureGeolocation.php
class SecureGeolocation extends ProductionGeolocation
{
    private array $trustedProxies;
    private RateLimiter $rateLimiter;

    public function __construct(
        CacheItemPoolInterface $cache,
        LoggerInterface $logger,
        RateLimiter $rateLimiter,
        array $config = [],
        array $server = null,
        array $countryToLanguage = [],
        string $cookieName = 'language'
    ) {
        $this->rateLimiter = $rateLimiter;
        $this->trustedProxies = $config['trusted_proxies'] ?? [];

        // Validate request before processing
        $this->validateRequest($server ?: $_SERVER);

        parent::__construct($cache, $logger, $config, $server, $countryToLanguage, $cookieName);
    }

    private function validateRequest(array $server): void
    {
        // Rate limiting
        $clientIP = $this->getClientIP();
        if (!$this->rateLimiter->isAllowed($clientIP)) {
            throw new SecurityException('Rate limit exceeded');
        }

        // Validate proxy chain
        if (!$this->isFromTrustedProxy()) {
            $this->logger->warning('Request from untrusted proxy', [
                'ip' => $clientIP,
                'cf_ray' => $server['HTTP_CF_RAY'] ?? null
            ]);
        }

        // Validate geolocation headers
        $this->validateGeolocationHeaders($server);

        // Check for header injection attempts
        $this->detectHeaderInjection($server);
    }

    private function isFromTrustedProxy(): bool
    {
        $clientIP = $_SERVER['REMOTE_ADDR'] ?? '';

        foreach ($this->trustedProxies as $range) {
            if ($this->ipInRange($clientIP, $range)) {
                return true;
            }
        }

        return false;
    }

    private function validateGeolocationHeaders(array $server): void
    {
        // Validate CF-IPCountry format
        if (isset($server['HTTP_CF_IPCOUNTRY'])) {
            $country = $server['HTTP_CF_IPCOUNTRY'];
            if (!preg_match('/^[A-Z]{2}$/', $country)) {
                $this->logger->warning('Invalid CF-IPCountry header', ['value' => $country]);
                unset($_SERVER['HTTP_CF_IPCOUNTRY']);
            }
        }

        // Validate CF-Ray format
        if (isset($server['HTTP_CF_RAY'])) {
            $ray = $server['HTTP_CF_RAY'];
            if (!preg_match('/^[a-f0-9]{16}-[A-Z]{3}$/', $ray)) {
                $this->logger->warning('Invalid CF-Ray header', ['value' => $ray]);
            }
        }
    }

    private function detectHeaderInjection(array $server): void
    {
        $dangerousPatterns = [
            '/\r?\n/', // CRLF injection
            '/<script/i', // XSS attempt
            '/javascript:/i', // JavaScript injection
            '/data:/i', // Data URI
            '/vbscript:/i' // VBScript injection
        ];

        $headers = [
            'HTTP_CF_IPCOUNTRY', 'HTTP_ACCEPT_LANGUAGE',
            'HTTP_USER_AGENT', 'HTTP_REFERER'
        ];

        foreach ($headers as $header) {
            if (isset($server[$header])) {
                $value = $server[$header];
                foreach ($dangerousPatterns as $pattern) {
                    if (preg_match($pattern, $value)) {
                        $this->logger->alert('Header injection detected', [
                            'header' => $header,
                            'value' => $value,
                            'pattern' => $pattern,
                            'ip' => $this->getClientIP()
                        ]);

                        throw new SecurityException('Malicious header detected');
                    }
                }
            }
        }
    }

    private function ipInRange(string $ip, string $range): bool
    {
        if (strpos($range, '/') === false) {
            return $ip === $range;
        }

        [$subnet, $mask] = explode('/', $range);
        return (ip2long($ip) & ~((1 << (32 - $mask)) - 1)) === ip2long($subnet);
    }
}

class SecurityException extends \Exception {}

// Rate Limiter implementation
class RateLimiter
{
    private CacheItemPoolInterface $cache;
    private int $maxRequests;
    private int $windowSeconds;

    public function __construct(CacheItemPoolInterface $cache, int $maxRequests = 1000, int $windowSeconds = 3600)
    {
        $this->cache = $cache;
        $this->maxRequests = $maxRequests;
        $this->windowSeconds = $windowSeconds;
    }

    public function isAllowed(string $identifier): bool
    {
        $key = 'rate_limit_' . md5($identifier);
        $cached = $this->cache->getItem($key);

        if (!$cached->isHit()) {
            $cached->set(1);
            $cached->expiresAfter($this->windowSeconds);
            $this->cache->save($cached);
            return true;
        }

        $count = $cached->get();
        if ($count >= $this->maxRequests) {
            return false;
        }

        $cached->set($count + 1);
        $this->cache->save($cached);
        return true;
    }
}

Monitoring and Logging

Comprehensive Monitoring Setup

<?php
// src/GeolocationMonitor.php
use Psr\Log\LoggerInterface;
use Prometheus\CollectorRegistry;
use Prometheus\Counter;
use Prometheus\Histogram;
use Prometheus\Gauge;

class GeolocationMonitor
{
    private LoggerInterface $logger;
    private CollectorRegistry $registry;
    private Counter $requestCounter;
    private Histogram $responseTime;
    private Gauge $cacheHitRate;
    private Counter $errorCounter;

    public function __construct(LoggerInterface $logger, CollectorRegistry $registry)
    {
        $this->logger = $logger;
        $this->registry = $registry;

        $this->initializeMetrics();
    }

    private function initializeMetrics(): void
    {
        $this->requestCounter = $this->registry->getOrRegisterCounter(
            'geolocation_requests_total',
            'Total number of geolocation requests',
            ['country', 'language', 'source']
        );

        $this->responseTime = $this->registry->getOrRegisterHistogram(
            'geolocation_request_duration_seconds',
            'Geolocation request duration',
            ['operation'],
            [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
        );

        $this->cacheHitRate = $this->registry->getOrRegisterGauge(
            'geolocation_cache_hit_rate',
            'Cache hit rate for geolocation requests'
        );

        $this->errorCounter = $this->registry->getOrRegisterCounter(
            'geolocation_errors_total',
            'Total number of geolocation errors',
            ['type', 'country']
        );
    }

    public function recordRequest(string $country, string $language, string $source): void
    {
        $this->requestCounter->inc([$country, $language, $source]);

        $this->logger->info('Geolocation request', [
            'country' => $country,
            'language' => $language,
            'source' => $source,
            'timestamp' => time()
        ]);
    }

    public function recordResponseTime(string $operation, float $duration): void
    {
        $this->responseTime->observe($duration, [$operation]);

        if ($duration > 0.1) { // Log slow requests
            $this->logger->warning('Slow geolocation operation', [
                'operation' => $operation,
                'duration' => $duration
            ]);
        }
    }

    public function updateCacheHitRate(float $rate): void
    {
        $this->cacheHitRate->set($rate);
    }

    public function recordError(string $type, string $country, \Exception $error): void
    {
        $this->errorCounter->inc([$type, $country]);

        $this->logger->error('Geolocation error', [
            'type' => $type,
            'country' => $country,
            'error' => $error->getMessage(),
            'trace' => $error->getTraceAsString()
        ]);
    }

    public function getHealthStatus(): array
    {
        $samples = $this->registry->getMetricFamilySamples();
        $health = ['status' => 'healthy', 'checks' => []];

        // Check error rate
        $errorRate = $this->calculateErrorRate($samples);
        $health['checks']['error_rate'] = [
            'status' => $errorRate < 0.05 ? 'pass' : 'fail',
            'value' => $errorRate,
            'threshold' => 0.05
        ];

        // Check cache hit rate
        $cacheHitRate = $this->getCacheHitRate($samples);
        $health['checks']['cache_hit_rate'] = [
            'status' => $cacheHitRate > 0.8 ? 'pass' : 'warn',
            'value' => $cacheHitRate,
            'threshold' => 0.8
        ];

        // Check average response time
        $avgResponseTime = $this->getAverageResponseTime($samples);
        $health['checks']['response_time'] = [
            'status' => $avgResponseTime < 0.1 ? 'pass' : 'warn',
            'value' => $avgResponseTime,
            'threshold' => 0.1
        ];

        // Overall status
        $failedChecks = array_filter($health['checks'], fn($check) => $check['status'] === 'fail');
        if (!empty($failedChecks)) {
            $health['status'] = 'unhealthy';
        } else {
            $warnChecks = array_filter($health['checks'], fn($check) => $check['status'] === 'warn');
            if (!empty($warnChecks)) {
                $health['status'] = 'degraded';
            }
        }

        return $health;
    }

    private function calculateErrorRate(array $samples): float
    {
        // Implementation depends on your metrics structure
        return 0.01; // Placeholder
    }

    private function getCacheHitRate(array $samples): float
    {
        // Implementation depends on your metrics structure
        return 0.85; // Placeholder
    }

    private function getAverageResponseTime(array $samples): float
    {
        // Implementation depends on your metrics structure
        return 0.05; // Placeholder
    }
}

Health Check Endpoint

<?php
// public/health.php
require_once '../vendor/autoload.php';

class GeolocationHealthCheck
{
    private GeolocationMonitor $monitor;
    private ProductionGeolocation $geo;

    public function __construct(GeolocationMonitor $monitor, ProductionGeolocation $geo)
    {
        $this->monitor = $monitor;
        $this->geo = $geo;
    }

    public function check(): array
    {
        $health = $this->monitor->getHealthStatus();

        // Add service-specific checks
        $health['checks']['cloudflare'] = $this->checkCloudFlare();
        $health['checks']['geolocation_service'] = $this->checkGeolocationService();
        $health['checks']['cache'] = $this->checkCache();

        // Add system information
        $health['system'] = [
            'php_version' => PHP_VERSION,
            'memory_usage' => memory_get_usage(true),
            'memory_peak' => memory_get_peak_usage(true),
            'timestamp' => date('c')
        ];

        return $health;
    }

    private function checkCloudFlare(): array
    {
        $cfRay = $_SERVER['HTTP_CF_RAY'] ?? null;
        $cfCountry = $_SERVER['HTTP_CF_IPCOUNTRY'] ?? null;

        return [
            'status' => ($cfRay && $cfCountry) ? 'pass' : 'fail',
            'cf_ray' => $cfRay,
            'cf_country' => $cfCountry,
            'message' => ($cfRay && $cfCountry) ? 'CloudFlare active' : 'CloudFlare not detected'
        ];
    }

    private function checkGeolocationService(): array
    {
        try {
            $start = microtime(true);
            $country = $this->geo->getCountryCode();
            $language = $this->geo->getLanguage();
            $duration = microtime(true) - $start;

            return [
                'status' => 'pass',
                'country' => $country,
                'language' => $language,
                'response_time' => $duration,
                'message' => 'Geolocation service working'
            ];
        } catch (\Exception $e) {
            return [
                'status' => 'fail',
                'error' => $e->getMessage(),
                'message' => 'Geolocation service failed'
            ];
        }
    }

    private function checkCache(): array
    {
        try {
            // Test cache connectivity
            $testKey = 'health_check_' . time();
            $this->geo->cache->getItem($testKey);

            return [
                'status' => 'pass',
                'message' => 'Cache accessible'
            ];
        } catch (\Exception $e) {
            return [
                'status' => 'fail',
                'error' => $e->getMessage(),
                'message' => 'Cache not accessible'
            ];
        }
    }
}

// Execute health check
header('Content-Type: application/json');

try {
    $monitor = new GeolocationMonitor($logger, $registry);
    $geo = new ProductionGeolocation($cache, $logger, $config);
    $healthCheck = new GeolocationHealthCheck($monitor, $geo);

    $health = $healthCheck->check();

    // Set appropriate HTTP status
    $statusCode = match($health['status']) {
        'healthy' => 200,
        'degraded' => 200,
        'unhealthy' => 503,
        default => 500
    };

    http_response_code($statusCode);
    echo json_encode($health, JSON_PRETTY_PRINT);

} catch (\Exception $e) {
    http_response_code(503);
    echo json_encode([
        'status' => 'unhealthy',
        'error' => $e->getMessage(),
        'timestamp' => date('c')
    ], JSON_PRETTY_PRINT);
}
?>

Error Handling and Fallbacks

Resilient Error Handling

<?php
// src/ResilientGeolocation.php
class ResilientGeolocation extends SecureGeolocation
{
    private array $fallbackProviders = [];
    private int $maxRetries = 3;
    private float $circuitBreakerThreshold = 0.5;
    private int $circuitBreakerWindow = 300; // 5 minutes

    public function addFallbackProvider(callable $provider, string $name): void
    {
        $this->fallbackProviders[$name] = $provider;
    }

    public function getCountryCode(): string
    {
        return $this->executeWithFallback('getCountryCode', function() {
            return parent::getCountryCode();
        });
    }

    public function getLanguage(): string
    {
        return $this->executeWithFallback('getLanguage', function() {
            return parent::getLanguage();
        });
    }

    private function executeWithFallback(string $operation, callable $primary): string
    {
        // Check circuit breaker
        if ($this->isCircuitBreakerOpen($operation)) {
            $this->logger->warning('Circuit breaker open for ' . $operation);
            return $this->executeFallback($operation);
        }

        $attempts = 0;
        $lastException = null;

        while ($attempts < $this->maxRetries) {
            try {
                $result = $primary();
                $this->recordSuccess($operation);
                return $result;

            } catch (\Exception $e) {
                $lastException = $e;
                $attempts++;

                $this->logger->warning('Geolocation attempt failed', [
                    'operation' => $operation,
                    'attempt' => $attempts,
                    'error' => $e->getMessage()
                ]);

                if ($attempts < $this->maxRetries) {
                    usleep(100000 * $attempts); // Exponential backoff
                }
            }
        }

        // All retries failed, record failure and use fallback
        $this->recordFailure($operation);

        $this->logger->error('All geolocation attempts failed', [
            'operation' => $operation,
            'attempts' => $attempts,
            'last_error' => $lastException?->getMessage()
        ]);

        return $this->executeFallback($operation);
    }

    private function executeFallback(string $operation): string
    {
        foreach ($this->fallbackProviders as $name => $provider) {
            try {
                $result = $provider($operation);
                if ($result) {
                    $this->logger->info('Fallback provider succeeded', [
                        'operation' => $operation,
                        'provider' => $name,
                        'result' => $result
                    ]);
                    return $result;
                }
            } catch (\Exception $e) {
                $this->logger->warning('Fallback provider failed', [
                    'operation' => $operation,
                    'provider' => $name,
                    'error' => $e->getMessage()
                ]);
            }
        }

        // All fallbacks failed, return configured default
        $default = $operation === 'getCountryCode'
            ? $this->config['fallback_country']
            : $this->config['fallback_language'];

        $this->logger->error('All fallback providers failed', [
            'operation' => $operation,
            'using_default' => $default
        ]);

        return $default;
    }

    private function isCircuitBreakerOpen(string $operation): bool
    {
        $key = "circuit_breaker_{$operation}";
        $cached = $this->cache->getItem($key);

        if (!$cached->isHit()) {
            return false;
        }

        $data = $cached->get();
        $errorRate = $data['errors'] / ($data['total'] ?: 1);

        return $errorRate > $this->circuitBreakerThreshold;
    }

    private function recordSuccess(string $operation): void
    {
        $this->updateCircuitBreakerStats($operation, true);
    }

    private function recordFailure(string $operation): void
    {
        $this->updateCircuitBreakerStats($operation, false);
    }

    private function updateCircuitBreakerStats(string $operation, bool $success): void
    {
        $key = "circuit_breaker_{$operation}";
        $cached = $this->cache->getItem($key);

        $data = $cached->isHit() ? $cached->get() : ['total' => 0, 'errors' => 0];

        $data['total']++;
        if (!$success) {
            $data['errors']++;
        }

        $cached->set($data);
        $cached->expiresAfter($this->circuitBreakerWindow);
        $this->cache->save($cached);
    }
}

// Example fallback providers
class GeolocationFallbacks
{
    public static function ipApiProvider(string $operation): ?string
    {
        if ($operation !== 'getCountryCode') {
            return null;
        }

        $ip = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
        if (!$ip || $ip === '127.0.0.1') {
            return null;
        }

        $url = "http://ip-api.com/json/{$ip}?fields=countryCode";
        $context = stream_context_create(['http' => ['timeout' => 2]]);
        $response = @file_get_contents($url, false, $context);

        if ($response) {
            $data = json_decode($response, true);
            return $data['countryCode'] ?? null;
        }

        return null;
    }

    public static function geoIPProvider(string $operation): ?string
    {
        if ($operation !== 'getCountryCode') {
            return null;
        }

        // Assuming GeoIP2 database is available
        if (extension_loaded('geoip')) {
            $ip = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
            return geoip_country_code_by_name($ip) ?: null;
        }

        return null;
    }

    public static function browserLanguageProvider(string $operation): ?string
    {
        if ($operation !== 'getLanguage') {
            return null;
        }

        $acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
        if (preg_match('/^([a-z]{2})/', $acceptLanguage, $matches)) {
            return $matches[1];
        }

        return null;
    }
}

// Setup with fallbacks
$geo = new ResilientGeolocation($cache, $logger, $rateLimiter, $config);
$geo->addFallbackProvider([GeolocationFallbacks::class, 'ipApiProvider'], 'ip-api');
$geo->addFallbackProvider([GeolocationFallbacks::class, 'geoIPProvider'], 'geoip');
$geo->addFallbackProvider([GeolocationFallbacks::class, 'browserLanguageProvider'], 'browser');

Caching Strategies

Multi-Layer Caching

<?php
// src/GeolocationCache.php
class GeolocationCache
{
    private array $drivers = [];
    private array $config;

    public function __construct(array $config = [])
    {
        $this->config = array_merge([
            'levels' => [
                'memory' => ['ttl' => 300, 'size' => 1000],
                'redis' => ['ttl' => 3600, 'cluster' => false],
                'file' => ['ttl' => 86400, 'path' => '/tmp/geocache']
            ]
        ], $config);

        $this->initializeDrivers();
    }

    private function initializeDrivers(): void
    {
        // Memory cache (fastest)
        $this->drivers['memory'] = new MemoryCache($this->config['levels']['memory']);

        // Redis cache (shared)
        if (extension_loaded('redis')) {
            $this->drivers['redis'] = new RedisCache($this->config['levels']['redis']);
        }

        // File cache (persistent)
        $this->drivers['file'] = new FileCache($this->config['levels']['file']);
    }

    public function get(string $key): ?string
    {
        // Try each cache level in order
        foreach ($this->drivers as $name => $driver) {
            $value = $driver->get($key);
            if ($value !== null) {
                // Populate higher-priority caches
                $this->backfillCaches($key, $value, $name);
                return $value;
            }
        }

        return null;
    }

    public function set(string $key, string $value): void
    {
        // Store in all cache levels
        foreach ($this->drivers as $driver) {
            $driver->set($key, $value);
        }
    }

    private function backfillCaches(string $key, string $value, string $foundIn): void
    {
        $found = false;

        foreach ($this->drivers as $name => $driver) {
            if ($name === $foundIn) {
                $found = true;
                continue;
            }

            if (!$found) {
                // This is a higher-priority cache that missed
                $driver->set($key, $value);
            }
        }
    }

    public function invalidate(string $pattern = '*'): void
    {
        foreach ($this->drivers as $driver) {
            $driver->invalidate($pattern);
        }
    }

    public function getStats(): array
    {
        $stats = [];
        foreach ($this->drivers as $name => $driver) {
            $stats[$name] = $driver->getStats();
        }
        return $stats;
    }
}

class MemoryCache
{
    private array $cache = [];
    private array $timestamps = [];
    private int $maxSize;
    private int $ttl;

    public function __construct(array $config)
    {
        $this->maxSize = $config['size'];
        $this->ttl = $config['ttl'];
    }

    public function get(string $key): ?string
    {
        if (!isset($this->cache[$key])) {
            return null;
        }

        if (time() - $this->timestamps[$key] > $this->ttl) {
            unset($this->cache[$key], $this->timestamps[$key]);
            return null;
        }

        return $this->cache[$key];
    }

    public function set(string $key, string $value): void
    {
        // Evict old entries if at capacity
        if (count($this->cache) >= $this->maxSize) {
            $this->evictOldest();
        }

        $this->cache[$key] = $value;
        $this->timestamps[$key] = time();
    }

    private function evictOldest(): void
    {
        $oldest = min($this->timestamps);
        $oldestKey = array_search($oldest, $this->timestamps);
        unset($this->cache[$oldestKey], $this->timestamps[$oldestKey]);
    }

    public function invalidate(string $pattern = '*'): void
    {
        if ($pattern === '*') {
            $this->cache = [];
            $this->timestamps = [];
        } else {
            // Pattern matching implementation
            $regex = str_replace('*', '.*', $pattern);
            foreach (array_keys($this->cache) as $key) {
                if (preg_match("/$regex/", $key)) {
                    unset($this->cache[$key], $this->timestamps[$key]);
                }
            }
        }
    }

    public function getStats(): array
    {
        return [
            'type' => 'memory',
            'size' => count($this->cache),
            'max_size' => $this->maxSize,
            'ttl' => $this->ttl
        ];
    }
}

Load Balancing Considerations

Load Balancer Configuration

# nginx-lb.conf
upstream geolocation_app {
    least_conn;
    server app1.internal:80 weight=3;
    server app2.internal:80 weight=3;
    server app3.internal:80 weight=2;

    # Health checks
    keepalive 32;
}

server {
    listen 80;
    server_name yourdomain.com;

    # Preserve CloudFlare headers
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $host;

    # CloudFlare geolocation headers
    proxy_set_header CF-IPCountry $http_cf_ipcountry;
    proxy_set_header CF-Ray $http_cf_ray;
    proxy_set_header CF-Visitor $http_cf_visitor;
    proxy_set_header CF-Connecting-IP $http_cf_connecting_ip;

    location / {
        proxy_pass http://geolocation_app;
        proxy_connect_timeout 5s;
        proxy_send_timeout 10s;
        proxy_read_timeout 10s;
    }

    location /health {
        proxy_pass http://geolocation_app;
        access_log off;
    }
}

Session Affinity for Geolocation

<?php
// src/GeolocationAffinity.php
class GeolocationAffinity
{
    public static function getAffinityKey(): string
    {
        // Use country + language for session affinity
        $geo = new Geolocation();
        $country = $geo->getCountryCode();
        $language = $geo->getLanguage();

        return "{$country}_{$language}";
    }

    public static function getPreferredServer(string $affinityKey): string
    {
        $servers = [
            'US_en' => 'us-east-1',
            'CA_en' => 'us-east-1',
            'CA_fr' => 'canada-1',
            'GB_en' => 'eu-west-1',
            'DE_de' => 'eu-central-1',
            'FR_fr' => 'eu-west-1',
            'JP_ja' => 'asia-pacific-1',
            'AU_en' => 'asia-pacific-1'
        ];

        return $servers[$affinityKey] ?? 'us-east-1';
    }
}

// HAProxy configuration example
/*
backend geolocation_backend
    balance roundrobin
    stick-table type string len 10 size 100k expire 1h
    stick on hdr(X-Geo-Affinity)

    server app1 10.0.1.10:80 check
    server app2 10.0.1.11:80 check
    server app3 10.0.1.12:80 check
*/

Next Steps


Previous: CloudFlare Setup | Next: Multi-language Websites

Clone this wiki locally