-
Notifications
You must be signed in to change notification settings - Fork 0
Production
Rumen Damyanov edited this page Jul 31, 2025
·
1 revision
Comprehensive guide for deploying and maintaining php-geolocation in production environments with performance, security, and monitoring considerations.
- Production Deployment Checklist
- Performance Optimization
- Security Considerations
- Monitoring and Logging
- Error Handling and Fallbacks
- Caching Strategies
- Load Balancing Considerations
- CDN Integration
- Database Considerations
- Backup and Recovery
#!/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 ✓ ==="<?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
]
];# 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<?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'
];
}
}<?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<?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;
}
}<?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
}
}<?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);
}
?><?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');<?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
];
}
}# 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;
}
}<?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
*/- 🌍 Multi-language Websites - International application patterns
- 📍 Geographic Content Delivery - Location-based content
- 📊 Analytics Integration - Geographic analytics
- 🛠️ Troubleshooting - Production issue resolution
Previous: CloudFlare Setup | Next: Multi-language Websites