Skip to content

Geo Content

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

Geographic Content Delivery

Advanced patterns for delivering location-specific content, pricing, features, and user experiences based on geographic location using php-geolocation.

Table of Contents

Content Personalization Strategy

Geographic Content Engine

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

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

class GeoContentEngine
{
    private Geolocation $geo;
    private CacheItemPoolInterface $cache;
    private LoggerInterface $logger;
    private array $contentRules = [];
    private array $regionalSettings = [];

    public function __construct(
        Geolocation $geo,
        CacheItemPoolInterface $cache,
        LoggerInterface $logger,
        array $config = []
    ) {
        $this->geo = $geo;
        $this->cache = $cache;
        $this->logger = $logger;
        $this->loadRegionalSettings($config);
    }

    private function loadRegionalSettings(array $config): void
    {
        $this->regionalSettings = array_merge([
            'US' => [
                'currency' => 'USD',
                'tax_inclusive' => false,
                'legal_drinking_age' => 21,
                'measurement_system' => 'imperial',
                'date_format' => 'M/d/Y',
                'phone_format' => '+1 (XXX) XXX-XXXX',
                'address_format' => 'us'
            ],
            'GB' => [
                'currency' => 'GBP',
                'tax_inclusive' => true,
                'legal_drinking_age' => 18,
                'measurement_system' => 'metric',
                'date_format' => 'd/m/Y',
                'phone_format' => '+44 XXXX XXXXXX',
                'address_format' => 'uk'
            ],
            'DE' => [
                'currency' => 'EUR',
                'tax_inclusive' => true,
                'legal_drinking_age' => 16,
                'measurement_system' => 'metric',
                'date_format' => 'd.m.Y',
                'phone_format' => '+49 XXX XXXXXXX',
                'address_format' => 'eu',
                'gdpr_required' => true
            ],
            'FR' => [
                'currency' => 'EUR',
                'tax_inclusive' => true,
                'legal_drinking_age' => 18,
                'measurement_system' => 'metric',
                'date_format' => 'd/m/Y',
                'phone_format' => '+33 X XX XX XX XX',
                'address_format' => 'eu',
                'gdpr_required' => true
            ],
            'JP' => [
                'currency' => 'JPY',
                'tax_inclusive' => true,
                'legal_drinking_age' => 20,
                'measurement_system' => 'metric',
                'date_format' => 'Y/m/d',
                'phone_format' => '+81-XX-XXXX-XXXX',
                'address_format' => 'jp'
            ],
            'CN' => [
                'currency' => 'CNY',
                'tax_inclusive' => true,
                'legal_drinking_age' => 18,
                'measurement_system' => 'metric',
                'date_format' => 'Y-m-d',
                'phone_format' => '+86 XXX XXXX XXXX',
                'address_format' => 'cn',
                'blocked_services' => ['google', 'facebook', 'twitter']
            ]
        ], $config['regional_settings'] ?? []);
    }

    public function addContentRule(string $name, callable $rule, int $priority = 100): void
    {
        $this->contentRules[] = [
            'name' => $name,
            'rule' => $rule,
            'priority' => $priority
        ];

        // Sort by priority (lower number = higher priority)
        usort($this->contentRules, fn($a, $b) => $a['priority'] <=> $b['priority']);
    }

    public function getPersonalizedContent(string $contentKey, array $context = []): array
    {
        $country = $this->geo->getCountryCode();
        $language = $this->geo->getLanguage();

        $cacheKey = "geo_content_{$contentKey}_{$country}_{$language}_" . md5(serialize($context));
        $cached = $this->cache->getItem($cacheKey);

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

        $baseContent = $this->getBaseContent($contentKey, $context);
        $personalizedContent = $this->applyGeographicPersonalization($baseContent, $country, $language, $context);

        // Cache the result
        $cached->set($personalizedContent);
        $cached->expiresAfter(3600);
        $this->cache->save($cached);

        $this->logger->info('Geographic content delivered', [
            'content_key' => $contentKey,
            'country' => $country,
            'language' => $language,
            'rules_applied' => count($this->contentRules)
        ]);

        return $personalizedContent;
    }

    private function getBaseContent(string $contentKey, array $context): array
    {
        // This would typically load from your content management system
        $content = [
            'title' => $context['title'] ?? 'Default Title',
            'description' => $context['description'] ?? 'Default Description',
            'features' => $context['features'] ?? [],
            'pricing' => $context['pricing'] ?? [],
            'legal_notices' => $context['legal_notices'] ?? [],
            'contact_info' => $context['contact_info'] ?? [],
            'payment_methods' => $context['payment_methods'] ?? ['credit_card'],
            'shipping_options' => $context['shipping_options'] ?? ['standard']
        ];

        return $content;
    }

    private function applyGeographicPersonalization(array $content, string $country, string $language, array $context): array
    {
        $regional = $this->getRegionalSettings($country);

        // Apply regional settings
        $content['regional_settings'] = $regional;
        $content['country'] = $country;
        $content['language'] = $language;

        // Apply content rules in priority order
        foreach ($this->contentRules as $ruleConfig) {
            $rule = $ruleConfig['rule'];
            $content = $rule($content, $country, $language, $regional, $context) ?: $content;
        }

        return $content;
    }

    public function getRegionalSettings(string $country): array
    {
        return $this->regionalSettings[$country] ?? $this->regionalSettings['US'];
    }

    public function isFeatureAvailable(string $feature, string $country = null): bool
    {
        $country = $country ?: $this->geo->getCountryCode();
        $regional = $this->getRegionalSettings($country);

        // Check if feature is blocked in this region
        $blockedFeatures = $regional['blocked_features'] ?? [];
        if (in_array($feature, $blockedFeatures)) {
            return false;
        }

        // Check feature-specific availability rules
        $availabilityRules = [
            'alcohol_sales' => function($regional) {
                return isset($regional['legal_drinking_age']);
            },
            'prescription_drugs' => function($regional) {
                return in_array($regional['pharmacy_regulations'] ?? 'strict', ['strict', 'regulated']);
            },
            'financial_services' => function($country) {
                $restrictedCountries = ['CN', 'KP', 'IR', 'SY'];
                return !in_array($country, $restrictedCountries);
            },
            'gambling' => function($country) {
                $allowedCountries = ['US', 'GB', 'AU', 'CA', 'DE', 'NL'];
                return in_array($country, $allowedCountries);
            }
        ];

        if (isset($availabilityRules[$feature])) {
            return $availabilityRules[$feature]($regional, $country);
        }

        return true; // Feature available by default
    }

    public function getLocalizedPricing(array $basePricing, string $country = null): array
    {
        $country = $country ?: $this->geo->getCountryCode();
        $regional = $this->getRegionalSettings($country);

        $localizedPricing = [];

        foreach ($basePricing as $item => $price) {
            $localPrice = $this->convertPrice($price, 'USD', $regional['currency']);

            // Apply regional adjustments
            $localPrice = $this->applyRegionalPricingAdjustments($localPrice, $country, $item);

            // Format according to regional preferences
            $localizedPricing[$item] = [
                'amount' => $localPrice,
                'currency' => $regional['currency'],
                'formatted' => $this->formatPrice($localPrice, $regional),
                'tax_inclusive' => $regional['tax_inclusive']
            ];
        }

        return $localizedPricing;
    }

    private function convertPrice(float $amount, string $fromCurrency, string $toCurrency): float
    {
        if ($fromCurrency === $toCurrency) {
            return $amount;
        }

        // In production, you'd use a real currency conversion API
        $exchangeRates = [
            'USD' => 1.0,
            'EUR' => 0.85,
            'GBP' => 0.73,
            'JPY' => 110.0,
            'CNY' => 6.4,
            'CAD' => 1.25,
            'AUD' => 1.35
        ];

        $usdAmount = $amount / ($exchangeRates[$fromCurrency] ?? 1.0);
        return $usdAmount * ($exchangeRates[$toCurrency] ?? 1.0);
    }

    private function applyRegionalPricingAdjustments(float $price, string $country, string $item): float
    {
        // Regional pricing adjustments based on market conditions
        $adjustments = [
            'US' => 1.0,
            'CA' => 1.05,
            'GB' => 1.15,  // Higher due to VAT and market conditions
            'DE' => 1.12,
            'FR' => 1.10,
            'JP' => 1.08,
            'AU' => 1.20,
            'BR' => 0.85,  // Lower purchasing power
            'IN' => 0.70,
            'CN' => 0.90
        ];

        $countryAdjustment = $adjustments[$country] ?? 1.0;

        // Category-specific adjustments
        $categoryAdjustments = [
            'luxury' => ['US' => 1.0, 'CN' => 1.30, 'JP' => 1.25],
            'software' => ['IN' => 0.60, 'BR' => 0.75, 'CN' => 0.80],
            'physical_goods' => ['AU' => 1.35, 'CA' => 1.15]
        ];

        // Apply category adjustment if applicable
        foreach ($categoryAdjustments as $category => $adjusts) {
            if (strpos($item, $category) !== false && isset($adjusts[$country])) {
                $countryAdjustment = $adjusts[$country];
                break;
            }
        }

        return round($price * $countryAdjustment, 2);
    }

    private function formatPrice(float $amount, array $regional): string
    {
        $currency = $regional['currency'];
        $symbols = [
            'USD' => '$',
            'EUR' => '',
            'GBP' => '£',
            'JPY' => '¥',
            'CNY' => '¥',
            'CAD' => 'C$',
            'AUD' => 'A$'
        ];

        $symbol = $symbols[$currency] ?? $currency;

        // Format based on regional preferences
        if ($currency === 'JPY') {
            // No decimals for JPY
            return $symbol . number_format($amount, 0);
        }

        // European format (€1.234,56)
        if (in_array($currency, ['EUR']) && in_array($regional['country'] ?? '', ['DE', 'FR', 'ES', 'IT'])) {
            return $symbol . number_format($amount, 2, ',', '.');
        }

        // Standard format ($1,234.56)
        return $symbol . number_format($amount, 2);
    }
}

Geographic Content Rules

Content Rule Examples

<?php
// Content personalization rules examples

$geoContent = new GeoContentEngine($geo, $cache, $logger, $config);

// Rule 1: Hide alcohol-related content in countries with restrictions
$geoContent->addContentRule('alcohol_restriction', function($content, $country, $language, $regional, $context) {
    $restrictedCountries = ['SA', 'AE', 'KW', 'QA', 'BH'];

    if (in_array($country, $restrictedCountries)) {
        // Remove alcohol-related content
        $content['features'] = array_filter($content['features'], function($feature) {
            return !str_contains(strtolower($feature), 'alcohol');
        });

        // Add compliance notice
        $content['legal_notices'][] = 'Alcohol-related content not available in your region';
    }

    return $content;
}, 10); // High priority

// Rule 2: GDPR compliance for EU countries
$geoContent->addContentRule('gdpr_compliance', function($content, $country, $language, $regional, $context) {
    $euCountries = ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE'];

    if (in_array($country, $euCountries)) {
        $content['legal_notices'][] = 'We comply with GDPR regulations for data protection';
        $content['cookie_consent_required'] = true;
        $content['data_retention_policy'] = 'We retain your data for no longer than necessary as per GDPR requirements';
    }

    return $content;
}, 20);

// Rule 3: Payment method availability by region
$geoContent->addContentRule('payment_methods', function($content, $country, $language, $regional, $context) {
    $paymentsByCountry = [
        'US' => ['credit_card', 'paypal', 'apple_pay', 'google_pay'],
        'DE' => ['credit_card', 'paypal', 'sofort', 'klarna', 'sepa'],
        'NL' => ['credit_card', 'paypal', 'ideal', 'klarna'],
        'CN' => ['alipay', 'wechat_pay', 'unionpay'],
        'JP' => ['credit_card', 'convenience_store', 'bank_transfer'],
        'BR' => ['credit_card', 'pix', 'boleto'],
        'IN' => ['credit_card', 'upi', 'paytm', 'razorpay']
    ];

    $content['payment_methods'] = $paymentsByCountry[$country] ?? ['credit_card', 'paypal'];

    return $content;
}, 30);

// Rule 4: Shipping options by geography
$geoContent->addContentRule('shipping_options', function($content, $country, $language, $regional, $context) {
    $shippingByRegion = [
        'domestic_us' => ['US' => ['standard', 'express', 'overnight', 'same_day']],
        'domestic_eu' => [
            'DE' => ['standard', 'express', 'dhl_premium'],
            'FR' => ['standard', 'express', 'chronopost'],
            'GB' => ['standard', 'express', 'royal_mail_tracked']
        ],
        'international' => ['standard_international', 'express_international']
    ];

    // Determine shipping options
    if ($country === 'US') {
        $content['shipping_options'] = $shippingByRegion['domestic_us']['US'];
    } elseif (isset($shippingByRegion['domestic_eu'][$country])) {
        $content['shipping_options'] = $shippingByRegion['domestic_eu'][$country];
    } else {
        $content['shipping_options'] = $shippingByRegion['international'];
    }

    return $content;
}, 40);

// Rule 5: Age verification requirements
$geoContent->addContentRule('age_verification', function($content, $country, $language, $regional, $context) {
    if (isset($content['age_restricted']) && $content['age_restricted']) {
        $drinkingAge = $regional['legal_drinking_age'] ?? 18;

        $content['age_verification'] = [
            'required' => true,
            'minimum_age' => $drinkingAge,
            'message' => "You must be at least {$drinkingAge} years old to access this content"
        ];
    }

    return $content;
}, 50);

Dynamic Pricing by Region

Regional Pricing Engine

<?php
// src/RegionalPricingEngine.php
class RegionalPricingEngine
{
    private GeoContentEngine $geoContent;
    private PDO $db;
    private array $pricingRules = [];

    public function __construct(GeoContentEngine $geoContent, PDO $db)
    {
        $this->geoContent = $geoContent;
        $this->db = $db;
        $this->loadPricingRules();
    }

    private function loadPricingRules(): void
    {
        // Load pricing rules from database
        $stmt = $this->db->query("
            SELECT country, product_category, markup_percentage, fixed_adjustment, currency_override
            FROM regional_pricing_rules
            WHERE active = 1
        ");

        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $this->pricingRules[$row['country']][$row['product_category']] = $row;
        }
    }

    public function calculateRegionalPrice(string $productId, float $basePrice, string $baseCurrency = 'USD'): array
    {
        $country = $this->geoContent->geo->getCountryCode();
        $regional = $this->geoContent->getRegionalSettings($country);

        // Get product category for pricing rules
        $category = $this->getProductCategory($productId);

        // Apply regional pricing rules
        $adjustedPrice = $this->applyPricingRules($basePrice, $country, $category);

        // Convert currency
        $targetCurrency = $regional['currency'];
        $localPrice = $this->geoContent->convertPrice($adjustedPrice, $baseCurrency, $targetCurrency);

        // Calculate tax if applicable
        $taxAmount = 0;
        if ($regional['tax_inclusive']) {
            $taxRate = $this->getTaxRate($country, $category);
            $taxAmount = $localPrice * ($taxRate / 100);
        }

        return [
            'base_price' => $basePrice,
            'base_currency' => $baseCurrency,
            'local_price' => $localPrice,
            'local_currency' => $targetCurrency,
            'tax_amount' => $taxAmount,
            'tax_rate' => $this->getTaxRate($country, $category),
            'total_price' => $localPrice + $taxAmount,
            'formatted_price' => $this->geoContent->formatPrice($localPrice + $taxAmount, $regional),
            'price_breakdown' => [
                'subtotal' => $localPrice,
                'tax' => $taxAmount,
                'total' => $localPrice + $taxAmount
            ]
        ];
    }

    private function applyPricingRules(float $price, string $country, string $category): float
    {
        $rule = $this->pricingRules[$country][$category] ?? null;

        if (!$rule) {
            // Apply default country rule if category-specific rule not found
            $rule = $this->pricingRules[$country]['default'] ?? null;
        }

        if ($rule) {
            // Apply percentage markup
            if ($rule['markup_percentage']) {
                $price *= (1 + $rule['markup_percentage'] / 100);
            }

            // Apply fixed adjustment
            if ($rule['fixed_adjustment']) {
                $price += $rule['fixed_adjustment'];
            }
        }

        return $price;
    }

    private function getProductCategory(string $productId): string
    {
        $stmt = $this->db->prepare("SELECT category FROM products WHERE id = ?");
        $stmt->execute([$productId]);
        return $stmt->fetchColumn() ?: 'default';
    }

    private function getTaxRate(string $country, string $category): float
    {
        $taxRates = [
            'US' => ['default' => 0, 'digital' => 6.25], // Varies by state
            'CA' => ['default' => 13, 'digital' => 13],   // HST/GST+PST
            'GB' => ['default' => 20, 'food' => 0],       // VAT
            'DE' => ['default' => 19, 'food' => 7],       // MwSt
            'FR' => ['default' => 20, 'food' => 5.5],     // TVA
            'JP' => ['default' => 10, 'food' => 8],       // Consumption tax
        ];

        $countryRates = $taxRates[$country] ?? ['default' => 0];
        return $countryRates[$category] ?? $countryRates['default'];
    }

    public function getPricingTiers(string $productId): array
    {
        $basePrice = $this->getBasePrice($productId);
        $tiers = [];

        // Calculate prices for major markets
        $majorMarkets = ['US', 'GB', 'DE', 'FR', 'JP', 'AU', 'CA'];

        foreach ($majorMarkets as $country) {
            $pricing = $this->calculateRegionalPrice($productId, $basePrice, 'USD');
            $tiers[$country] = $pricing;
        }

        return $tiers;
    }

    private function getBasePrice(string $productId): float
    {
        $stmt = $this->db->prepare("SELECT base_price FROM products WHERE id = ?");
        $stmt->execute([$productId]);
        return (float) $stmt->fetchColumn();
    }
}

// Database schema for regional pricing
/*
CREATE TABLE regional_pricing_rules (
    id INT PRIMARY KEY AUTO_INCREMENT,
    country CHAR(2) NOT NULL,
    product_category VARCHAR(50) NOT NULL DEFAULT 'default',
    markup_percentage DECIMAL(5,2) DEFAULT 0,
    fixed_adjustment DECIMAL(10,2) DEFAULT 0,
    currency_override CHAR(3) NULL,
    active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    UNIQUE KEY unique_country_category (country, product_category)
);

INSERT INTO regional_pricing_rules (country, product_category, markup_percentage) VALUES
('GB', 'default', 15.00),
('DE', 'default', 12.00),
('AU', 'default', 20.00),
('JP', 'luxury', 25.00),
('CN', 'software', -20.00),
('IN', 'software', -30.00),
('BR', 'default', -15.00);
*/

Localized Product Catalogs

Geographic Product Manager

<?php
// src/GeographicProductManager.php
class GeographicProductManager
{
    private GeoContentEngine $geoContent;
    private RegionalPricingEngine $pricing;
    private PDO $db;
    private CacheItemPoolInterface $cache;

    public function __construct(
        GeoContentEngine $geoContent,
        RegionalPricingEngine $pricing,
        PDO $db,
        CacheItemPoolInterface $cache
    ) {
        $this->geoContent = $geoContent;
        $this->pricing = $pricing;
        $this->db = $db;
        $this->cache = $cache;
    }

    public function getLocalizedCatalog(array $filters = []): array
    {
        $country = $this->geoContent->geo->getCountryCode();
        $language = $this->geoContent->geo->getLanguage();

        $cacheKey = "catalog_{$country}_{$language}_" . md5(serialize($filters));
        $cached = $this->cache->getItem($cacheKey);

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

        $products = $this->getAvailableProducts($country, $filters);
        $localizedProducts = [];

        foreach ($products as $product) {
            $localizedProduct = $this->localizeProduct($product, $country, $language);
            if ($localizedProduct) {
                $localizedProducts[] = $localizedProduct;
            }
        }

        // Sort by regional preferences
        $localizedProducts = $this->sortByRegionalPreferences($localizedProducts, $country);

        $result = [
            'products' => $localizedProducts,
            'total_count' => count($localizedProducts),
            'country' => $country,
            'language' => $language,
            'regional_info' => $this->geoContent->getRegionalSettings($country)
        ];

        $cached->set($result);
        $cached->expiresAfter(1800); // 30 minutes
        $this->cache->save($cached);

        return $result;
    }

    private function getAvailableProducts(string $country, array $filters): array
    {
        $sql = "
            SELECT DISTINCT p.id, p.sku, p.name, p.category, p.base_price, p.images, p.features
            FROM products p
            LEFT JOIN product_restrictions pr ON p.id = pr.product_id
            WHERE p.active = 1
            AND (pr.restricted_country IS NULL OR pr.restricted_country != ?)
        ";

        $params = [$country];

        // Apply category filter
        if (!empty($filters['category'])) {
            $sql .= " AND p.category = ?";
            $params[] = $filters['category'];
        }

        // Apply price range filter
        if (!empty($filters['price_min']) || !empty($filters['price_max'])) {
            // This is simplified - you'd want to convert prices to local currency first
            if (!empty($filters['price_min'])) {
                $sql .= " AND p.base_price >= ?";
                $params[] = $filters['price_min'];
            }
            if (!empty($filters['price_max'])) {
                $sql .= " AND p.base_price <= ?";
                $params[] = $filters['price_max'];
            }
        }

        // Apply feature requirements (e.g., age restrictions)
        if ($this->requiresAgeVerification($country)) {
            $sql .= " AND p.age_restricted = 0";
        }

        $sql .= " ORDER BY p.featured DESC, p.sort_order ASC";

        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);

        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function localizeProduct(array $product, string $country, string $language): ?array
    {
        // Check if product is available in this country
        if (!$this->isProductAvailable($product['id'], $country)) {
            return null;
        }

        // Get localized content
        $localizedContent = $this->getLocalizedProductContent($product['id'], $language);

        // Calculate regional pricing
        $pricing = $this->pricing->calculateRegionalPrice(
            $product['id'],
            $product['base_price']
        );

        // Apply regional feature modifications
        $features = $this->localizeFeatures($product['features'], $country);

        return [
            'id' => $product['id'],
            'sku' => $product['sku'],
            'name' => $localizedContent['name'] ?: $product['name'],
            'description' => $localizedContent['description'],
            'short_description' => $localizedContent['short_description'],
            'category' => $product['category'],
            'images' => $this->localizeImages($product['images'], $country),
            'features' => $features,
            'pricing' => $pricing,
            'availability' => $this->getAvailabilityInfo($product['id'], $country),
            'shipping_info' => $this->getShippingInfo($product['id'], $country),
            'compliance_info' => $this->getComplianceInfo($product['id'], $country)
        ];
    }

    private function isProductAvailable(string $productId, string $country): bool
    {
        $stmt = $this->db->prepare("
            SELECT COUNT(*) FROM product_restrictions
            WHERE product_id = ? AND restricted_country = ?
        ");
        $stmt->execute([$productId, $country]);

        return $stmt->fetchColumn() == 0;
    }

    private function getLocalizedProductContent(string $productId, string $language): array
    {
        $stmt = $this->db->prepare("
            SELECT name, description, short_description, specifications
            FROM product_localizations
            WHERE product_id = ? AND language = ?
        ");
        $stmt->execute([$productId, $language]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$result) {
            // Fallback to English
            $stmt->execute([$productId, 'en']);
            $result = $stmt->fetch(PDO::FETCH_ASSOC);
        }

        return $result ?: [];
    }

    private function localizeFeatures(string $featuresJson, string $country): array
    {
        $features = json_decode($featuresJson, true) ?: [];
        $regional = $this->geoContent->getRegionalSettings($country);

        // Convert measurements
        if ($regional['measurement_system'] === 'imperial') {
            $features = $this->convertToImperial($features);
        }

        // Remove region-restricted features
        $features = array_filter($features, function($feature) use ($country) {
            return $this->geoContent->isFeatureAvailable($feature, $country);
        });

        return $features;
    }

    private function convertToImperial(array $features): array
    {
        foreach ($features as &$feature) {
            // Convert weight (kg to lbs)
            $feature = preg_replace_callback('/(\d+(?:\.\d+)?)\s*kg/', function($matches) {
                $kg = floatval($matches[1]);
                $lbs = round($kg * 2.20462, 1);
                return $lbs . ' lbs';
            }, $feature);

            // Convert dimensions (cm to inches)
            $feature = preg_replace_callback('/(\d+(?:\.\d+)?)\s*cm/', function($matches) {
                $cm = floatval($matches[1]);
                $inches = round($cm / 2.54, 1);
                return $inches . '"';
            }, $feature);

            // Convert temperature (C to F)
            $feature = preg_replace_callback('/(\d+(?:\.\d+)?)\s*°C/', function($matches) {
                $celsius = floatval($matches[1]);
                $fahrenheit = round(($celsius * 9/5) + 32, 1);
                return $fahrenheit . '°F';
            }, $feature);
        }

        return $features;
    }

    private function localizeImages(string $imagesJson, string $country): array
    {
        $images = json_decode($imagesJson, true) ?: [];

        // Check for region-specific images
        foreach ($images as &$image) {
            $regionalImage = str_replace('.jpg', "_{$country}.jpg", $image);
            if (file_exists('public/images/' . $regionalImage)) {
                $image = $regionalImage;
            }
        }

        return $images;
    }

    private function getAvailabilityInfo(string $productId, string $country): array
    {
        $stmt = $this->db->prepare("
            SELECT in_stock, estimated_delivery_days, stock_location
            FROM product_availability
            WHERE product_id = ? AND country = ?
        ");
        $stmt->execute([$productId, $country]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        return $result ?: ['in_stock' => false, 'estimated_delivery_days' => null];
    }

    private function getShippingInfo(string $productId, string $country): array
    {
        $regional = $this->geoContent->getRegionalSettings($country);

        return [
            'available_methods' => $this->getShippingMethods($country),
            'free_shipping_threshold' => $this->getFreeShippingThreshold($country),
            'estimated_delivery' => $this->getEstimatedDelivery($productId, $country),
            'currency' => $regional['currency']
        ];
    }

    private function getShippingMethods(string $country): array
    {
        $methods = [
            'US' => ['standard' => '5-7 days', 'express' => '2-3 days', 'overnight' => '1 day'],
            'CA' => ['standard' => '7-10 days', 'express' => '3-5 days'],
            'GB' => ['standard' => '3-5 days', 'express' => '1-2 days'],
            'DE' => ['standard' => '2-4 days', 'express' => '1-2 days'],
            'AU' => ['standard' => '5-8 days', 'express' => '2-4 days']
        ];

        return $methods[$country] ?? ['standard' => '7-14 days'];
    }

    private function getFreeShippingThreshold(string $country): float
    {
        $thresholds = [
            'US' => 50.0,
            'CA' => 75.0,
            'GB' => 40.0,
            'DE' => 50.0,
            'AU' => 100.0
        ];

        return $thresholds[$country] ?? 100.0;
    }

    private function getEstimatedDelivery(string $productId, string $country): string
    {
        // This would typically consider product location, shipping method, etc.
        $baseDays = 5;

        $countryAdjustments = [
            'US' => 0,
            'CA' => 2,
            'GB' => 1,
            'DE' => 1,
            'AU' => 3,
            'JP' => 2
        ];

        $adjustedDays = $baseDays + ($countryAdjustments[$country] ?? 7);

        return "{$adjustedDays}-" . ($adjustedDays + 3) . " business days";
    }

    private function getComplianceInfo(string $productId, string $country): array
    {
        $compliance = [];
        $regional = $this->geoContent->getRegionalSettings($country);

        // GDPR compliance for EU
        if ($regional['gdpr_required'] ?? false) {
            $compliance['gdpr'] = 'This product complies with GDPR data protection requirements';
        }

        // CE marking for EU
        $euCountries = ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE'];
        if (in_array($country, $euCountries)) {
            $compliance['ce_marking'] = 'This product carries CE marking for European compliance';
        }

        // FCC compliance for US
        if ($country === 'US') {
            $compliance['fcc'] = 'This product complies with FCC regulations';
        }

        return $compliance;
    }

    private function sortByRegionalPreferences(array $products, string $country): array
    {
        // Sort products based on regional preferences
        $preferences = [
            'US' => ['featured', 'price_low_to_high'],
            'DE' => ['quality', 'brand'],
            'JP' => ['innovation', 'compact_size'],
            'CN' => ['value_for_money', 'local_brand']
        ];

        $countryPreferences = $preferences[$country] ?? ['featured'];

        // Apply sorting logic based on preferences
        usort($products, function($a, $b) use ($countryPreferences) {
            foreach ($countryPreferences as $preference) {
                $comparison = $this->compareByPreference($a, $b, $preference);
                if ($comparison !== 0) {
                    return $comparison;
                }
            }
            return 0;
        });

        return $products;
    }

    private function compareByPreference(array $a, array $b, string $preference): int
    {
        switch ($preference) {
            case 'price_low_to_high':
                return $a['pricing']['total_price'] <=> $b['pricing']['total_price'];
            case 'price_high_to_low':
                return $b['pricing']['total_price'] <=> $a['pricing']['total_price'];
            case 'featured':
                return ($b['featured'] ?? 0) <=> ($a['featured'] ?? 0);
            default:
                return 0;
        }
    }

    private function requiresAgeVerification(string $country): bool
    {
        // Countries with strict age verification requirements
        $strictCountries = ['US', 'GB', 'AU'];
        return in_array($country, $strictCountries);
    }
}

Geographic Feature Flags

Feature Flag Manager

<?php
// src/GeographicFeatureFlags.php
class GeographicFeatureFlags
{
    private GeoContentEngine $geoContent;
    private CacheItemPoolInterface $cache;
    private array $flags = [];

    public function __construct(GeoContentEngine $geoContent, CacheItemPoolInterface $cache)
    {
        $this->geoContent = $geoContent;
        $this->cache = $cache;
        $this->loadFeatureFlags();
    }

    private function loadFeatureFlags(): void
    {
        // Load from configuration or database
        $this->flags = [
            'payment_methods' => [
                'apple_pay' => ['US', 'CA', 'GB', 'AU'],
                'google_pay' => ['US', 'CA', 'GB', 'AU', 'DE', 'FR'],
                'paypal' => ['US', 'CA', 'GB', 'AU', 'DE', 'FR', 'ES', 'IT'],
                'alipay' => ['CN', 'HK', 'SG'],
                'wechat_pay' => ['CN'],
                'sofort' => ['DE', 'AT', 'CH'],
                'ideal' => ['NL'],
                'klarna' => ['SE', 'NO', 'FI', 'DK', 'DE', 'AT', 'NL', 'GB', 'US']
            ],
            'features' => [
                'live_chat' => ['US', 'CA', 'GB', 'AU', 'DE', 'FR'],
                'phone_support' => ['US', 'CA', 'GB'],
                'cryptocurrency_payment' => ['US', 'CA', 'GB', 'DE', 'NL', 'CH'],
                'subscription_service' => ['US', 'CA', 'GB', 'AU', 'DE', 'FR', 'ES', 'IT'],
                'same_day_delivery' => ['US', 'GB'],
                'installment_payments' => ['US', 'DE', 'SE', 'NO'],
                'social_login' => ['US', 'CA', 'GB', 'AU', 'DE', 'FR'],
                'gdpr_compliance_mode' => ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE']
            ],
            'content_types' => [
                'alcohol_content' => ['US', 'CA', 'GB', 'AU', 'DE', 'FR', 'ES', 'IT'],
                'gambling_content' => ['US', 'GB', 'AU', 'CA'],
                'cryptocurrency_content' => ['US', 'CA', 'GB', 'DE', 'NL', 'CH', 'SE'],
                'adult_content' => ['US', 'CA', 'GB', 'AU', 'DE', 'NL']
            ],
            'regulatory' => [
                'cookie_consent_banner' => ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE'],
                'age_verification_required' => ['US', 'GB', 'AU'],
                'tax_calculation_required' => ['US', 'CA', 'AU', 'GB', 'DE', 'FR'],
                'data_localization_required' => ['RU', 'CN', 'IN']
            ]
        ];
    }

    public function isEnabled(string $feature, string $country = null): bool
    {
        $country = $country ?: $this->geoContent->geo->getCountryCode();

        $cacheKey = "feature_flag_{$feature}_{$country}";
        $cached = $this->cache->getItem($cacheKey);

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

        $enabled = $this->checkFeatureFlag($feature, $country);

        $cached->set($enabled);
        $cached->expiresAfter(3600);
        $this->cache->save($cached);

        return $enabled;
    }

    private function checkFeatureFlag(string $feature, string $country): bool
    {
        // Check each category for the feature
        foreach ($this->flags as $category => $features) {
            if (isset($features[$feature])) {
                return in_array($country, $features[$feature]);
            }
        }

        // Feature not found - default to enabled
        return true;
    }

    public function getEnabledFeatures(string $country = null): array
    {
        $country = $country ?: $this->geoContent->geo->getCountryCode();
        $enabled = [];

        foreach ($this->flags as $category => $features) {
            foreach ($features as $feature => $countries) {
                if (in_array($country, $countries)) {
                    $enabled[$category][] = $feature;
                }
            }
        }

        return $enabled;
    }

    public function getPaymentMethods(string $country = null): array
    {
        $country = $country ?: $this->geoContent->geo->getCountryCode();
        $methods = [];

        foreach ($this->flags['payment_methods'] as $method => $countries) {
            if (in_array($country, $countries)) {
                $methods[] = $method;
            }
        }

        return $methods;
    }

    public function requiresGDPRCompliance(string $country = null): bool
    {
        return $this->isEnabled('gdpr_compliance_mode', $country);
    }

    public function requiresCookieConsent(string $country = null): bool
    {
        return $this->isEnabled('cookie_consent_banner', $country);
    }

    public function allowsContentType(string $contentType, string $country = null): bool
    {
        return $this->isEnabled($contentType, $country);
    }

    public function getRegionalConfig(string $country = null): array
    {
        $country = $country ?: $this->geoContent->geo->getCountryCode();

        return [
            'country' => $country,
            'enabled_features' => $this->getEnabledFeatures($country),
            'payment_methods' => $this->getPaymentMethods($country),
            'gdpr_required' => $this->requiresGDPRCompliance($country),
            'cookie_consent_required' => $this->requiresCookieConsent($country),
            'age_verification_required' => $this->isEnabled('age_verification_required', $country),
            'tax_calculation_required' => $this->isEnabled('tax_calculation_required', $country)
        ];
    }
}

// Usage example
$featureFlags = new GeographicFeatureFlags($geoContent, $cache);

// Check specific features
if ($featureFlags->isEnabled('live_chat')) {
    // Show live chat widget
}

if ($featureFlags->isEnabled('apple_pay')) {
    // Show Apple Pay option
}

if ($featureFlags->requiresGDPRCompliance()) {
    // Implement GDPR compliance measures
}

// Get all available payment methods for current country
$paymentMethods = $featureFlags->getPaymentMethods();

// Get complete regional configuration
$regionalConfig = $featureFlags->getRegionalConfig();

Next Steps


Previous: Multi-language Websites | Next: Analytics Integration

Clone this wiki locally