-
Notifications
You must be signed in to change notification settings - Fork 0
Geo Content
Rumen Damyanov edited this page Jul 31, 2025
·
1 revision
Advanced patterns for delivering location-specific content, pricing, features, and user experiences based on geographic location using php-geolocation.
- Content Personalization Strategy
- Geographic Content Rules
- Dynamic Pricing by Region
- Localized Product Catalogs
- Geographic Feature Flags
- Regional Compliance
- Content Delivery Networks
- A/B Testing by Geography
- Performance Optimization
- Implementation Examples
<?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);
}
}<?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);<?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);
*/<?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);
}
}<?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();- 📊 Analytics Integration - Geographic analytics and insights
- 🎯 API Development Patterns - RESTful APIs with geolocation
- 🔧 Configuration Reference - Complete configuration options
⚠️ Error Handling - Comprehensive error handling patterns
Previous: Multi-language Websites | Next: Analytics Integration