A high-performance PHP service for detecting user geolocation using Cloudflare headers with MaxMind GeoIP database fallback.
CFGeolocationService is designed to provide fast and accurate geolocation detection by leveraging Cloudflare's edge network capabilities while maintaining reliability through GeoIP database fallbacks. This dual-strategy approach ensures optimal performance and accuracy for web applications behind Cloudflare's proxy network.
- π Performance: Prioritizes Cloudflare headers for instant geolocation (no database lookup required)
- π‘οΈ Reliability: Falls back to MaxMind GeoIP2 database when Cloudflare headers are unavailable
- π Accuracy: Uses Cloudflare's global edge network data combined with MaxMind's proven database
- β‘ Speed: Minimal overhead with intelligent caching strategy
- π Security: Validates all inputs and handles edge cases gracefully
1. Check CF-IPCountry header β Valid ISO country code? β Return country
β
2. Check CF-Connecting-IP header β Valid IP address? β Use for GeoIP lookup
β
3. Fall back to Symfony getClientIp() β Perform GeoIP database lookup β Return country
-
Primary: Cloudflare Headers (instant results)
CF-IPCountry: ISO 3166-1 alpha-2 country codeCF-Connecting-IP: Real client IP address
-
Fallback: GeoIP Database Lookup
- MaxMind GeoLite2/GeoIP2 Country database
- Handles cases where CF headers are missing or invalid
composer require gryfoss/http-cf-geolocation-service- PHP: 8.1 or higher
- Extensions: mbstring, filter
- Dependencies:
symfony/http-foundation^7.3geoip2/geoip2^3.2
<?php
use GryfOSS\Geolocation\CFGeolocationService;
use Symfony\Component\HttpFoundation\Request;
// Initialize with GeoIP database path
$service = new CFGeolocationService('/path/to/GeoLite2-Country.mmdb');
// Get current request
$request = Request::createFromGlobals();
// Detect client IP address
$clientIp = $service->getIp($request);
echo "Client IP: " . $clientIp; // e.g., "203.0.113.1"
// Detect country code
$countryCode = $service->getCountryCode($request);
echo "Country: " . $countryCode; // e.g., "US"When your application is behind Cloudflare, the service automatically uses CF headers:
// Cloudflare provides these headers:
// CF-Connecting-IP: 203.0.113.1
// CF-IPCountry: US
$countryCode = $service->getCountryCode($request);
// Returns "US" instantly (no database lookup needed)
$clientIp = $service->getIp($request);
// Returns "203.0.113.1" (real client IP, not Cloudflare's)The service gracefully falls back to GeoIP database lookup:
// No CF headers available
$countryCode = $service->getCountryCode($request);
// Performs GeoIP database lookup on client IP
// Returns country code based on IP geolocationWhen developing on localhost or behind corporate VPNs, Cloudflare headers might be unavailable. Enable debug mode to short-circuit lookups and return deterministic values without changing your stack:
$service->setDebugMode(true, '203.0.113.77', 'PL'); // Use fake IP + ISO code
$clientIp = $service->getIp($request); // "203.0.113.77"
$countryCode = $service->getCountryCode($request); // "PL"
$service->setDebugMode(false); // Revert to normal detectionUse isDebugModeEnabled(), getDebugModeIp(), and getDebugModeCountryCode() to inspect the current state. Debug mode strictly validates both the IP address and ISO 3166-1 alpha-2 country code to avoid accidental misuse.
use GryfOSS\Geolocation\CFGeolocationService;
try {
$service = new CFGeolocationService('/invalid/path/database.mmdb');
} catch (\InvalidArgumentException $e) {
echo "Database not found: " . $e->getMessage();
}
try {
$countryCode = $service->getCountryCode($request);
} catch (\Exception $e) {
echo "Geolocation failed: " . $e->getMessage();
}See examples.php for comprehensive usage examples including:
- Basic geolocation detection
- Cloudflare header simulation
- GeoIP database fallback scenarios
- Error handling demonstrations
- Testing with various IP addresses
# Run examples (requires database download first)
./pull-free-db.sh
php examples.php# Download free GeoLite2 database
./pull-free-db.sh
# Or manually download from:
# https://dev.maxmind.com/geoip/geoip2/geolite2/# Install development dependencies
composer install
# Download test database
./pull-free-db.sh
# Run complete test suite
./vendor/bin/phpunit
# Run Behat functional specs (debug mode scenarios)
./vendor/bin/behat
# Run tests with coverage
XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-text
# Run tests with readable output
./vendor/bin/phpunit --testdox
# Run usage examples
php examples.phpThe project maintains 100% code coverage:
- β Lines: 100%
- β Methods: 100%
- β Classes: 100%
# Verify coverage locally
XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html coverage-report
# Open coverage-report/index.html in your browser- Unit Tests: 32 unit tests covering all methods and edge cases
- Integration Tests: 6 integration tests with real GeoIP database
- Total: 38 tests with 47 assertions
- Data Providers: Multiple test scenarios for various input combinations
- Exception Testing: Error handling and edge case validation
- Real Database: Tests use actual GeoLite2-Country.mmdb for authenticity
This package does not include the MaxMind GeoIP database due to licensing restrictions:
- GeoLite2 databases are available for free but require separate download
- Commercial GeoIP2 databases require a MaxMind license
- Distribution restrictions prevent bundling databases with open-source packages
# Use the included script
./pull-free-db.sh
# Manual download
wget https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb- Create account at MaxMind
- Generate license key
- Download GeoLite2 or purchase GeoIP2 databases
- Follow MaxMind's terms of service
# Set up cron job for monthly updates
0 0 1 * * /path/to/your/project/pull-free-db.sh// Common database locations
$service = new CFGeolocationService('/usr/local/share/GeoIP/GeoLite2-Country.mmdb');
$service = new CFGeolocationService('/var/lib/GeoIP/GeoLite2-Country.mmdb');
$service = new CFGeolocationService('./data/GeoLite2-Country.mmdb');# Set database path via environment
export GEOIP_DATABASE_PATH="/path/to/GeoLite2-Country.mmdb"// Use in your application
$databasePath = $_ENV['GEOIP_DATABASE_PATH'] ?? '/default/path/GeoLite2-Country.mmdb';
$service = new CFGeolocationService($databasePath);# config/services.yaml
services:
GryfOSS\Geolocation\CFGeolocationService:
arguments:
$databasePath: '%env(GEOIP_DATABASE_PATH)%'// config/app.php or service provider
$this->app->singleton(CFGeolocationService::class, function ($app) {
return new CFGeolocationService(env('GEOIP_DATABASE_PATH'));
});- Multi-PHP Testing: 8.2, 8.3, 8.4
- Automated Database Download: Fresh GeoLite2 database for each test run
- 100% Coverage Enforcement: Builds fail if coverage drops below 100%
- Cross-Platform: Ubuntu latest with comprehensive test matrix
- PSR-4 Autoloading: Namespace compliance
- PHPDoc Documentation: Comprehensive method and class documentation
- Type Declarations: Strict typing for all parameters and returns
- Error Handling: Graceful exception handling for all failure modes
- Input Validation: Thorough validation of all external inputs
We welcome contributions from the community! Here's how you can help:
- Bug Reports: Use the GitHub Issues to report bugs
- Feature Requests: Propose new features or improvements
- Security Issues: Report security vulnerabilities privately
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes with tests
- Ensure 100% test coverage:
XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-text - Follow coding standards: PSR-12 compliance
- Submit a pull request with detailed description
- Write tests first: TDD approach preferred
- Maintain 100% coverage: All new code must be fully tested
- Follow existing patterns: Consistent with current codebase
- Document thoroughly: PHPDoc for all public methods
- Validate inputs: Handle edge cases gracefully
Please follow our code of conduct in all interactions:
- Be respectful and inclusive
- Focus on constructive feedback
- Help others learn and grow
- Maintain professionalism
This project is licensed under the MIT License - see the LICENSE file for details.
- IDCT Bartosz PachoΕek - GitHub - Initial work
- MaxMind for providing GeoIP2 and GeoLite2 databases
- Cloudflare for their excellent geolocation headers
- Symfony for the robust HTTP foundation component
- P3TERX for maintaining the GeoLite2 mirror repository
- PHP Community for continuous improvements and feedback
Made with β€οΈ by the GryfOSS team