PHPackages                             arraypress/visitor-country - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. arraypress/visitor-country

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

arraypress/visitor-country
==========================

Resolve a visitor's country from CDN and server-level request headers (Cloudflare, AWS CloudFront, Fastly, BunnyCDN, mod\_geoip). Zero dependencies, framework-agnostic, signature-gated to defeat trivial spoofing.

1.0.0(1mo ago)01GPL-2.0-or-laterPHPPHP &gt;=7.4

Since May 6Pushed 1mo agoCompare

[ Source](https://github.com/arraypress/visitor-country)[ Packagist](https://packagist.org/packages/arraypress/visitor-country)[ Docs](https://github.com/arraypress/visitor-country/)[ RSS](/packages/arraypress-visitor-country/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)DependenciesVersions (2)Used By (0)

Visitor Country
===============

[](#visitor-country)

Resolve a visitor's country (ISO-3166 alpha-2) from CDN and server-level request headers.

Zero dependencies, framework-agnostic, signature-gated to defeat trivial spoofing. Bundles sources for Cloudflare, AWS CloudFront, Fastly, BunnyCDN, server-level GeoIP modules (Apache `mod_geoip` / nginx GeoIP), and a generic `X-Country-Code` fallback. Register your own sources for custom GeoIP databases.

Why
---

[](#why)

Most fraud-detection / analytics / geo-routing code reaches for a paid IP-lookup API to answer "where is this visitor from?" — but if your site sits behind Cloudflare, CloudFront, Fastly, or any other major CDN, the answer's already in the request headers, **for free**. This library reads those headers in the right order with the right safety checks, and returns the country code.

Install
-------

[](#install)

```
composer require arraypress/visitor-country
```

Usage
-----

[](#usage)

```
use ArrayPress\VisitorCountry\Country;

// Simple — returns "GB" or empty string on miss
$country = Country::resolve();

// With provenance — useful for logging / debugging
$result = Country::resolve_detailed();
$result->get_country();    // 'GB'
$result->get_source();     // 'cloudflare'
$result->get_confidence(); // 'high'
$result->has_country();    // true
$result->to_array();       // [ 'country' => 'GB', 'source' => 'cloudflare', 'confidence' => 'high' ]

// Pass a custom server array (testability)
Country::resolve( [ 'HTTP_CF_IPCOUNTRY' => 'GB', 'HTTP_CF_RAY' => 'abc' ] );
```

Sources
-------

[](#sources)

Tried in this order by default. First hit wins.

SourceHeaderConfidenceSig-gated?Cloudflare`HTTP_CF_IPCOUNTRY`highyes (`CF-Ray` / `CF-Visitor` / `CF-Connecting-IP`)AWS CloudFront`HTTP_CLOUDFRONT_VIEWER_COUNTRY`highnoFastly`HTTP_FASTLY_CLIENT_COUNTRY`highyes (`Fastly-FF` / `Fastly-Client-IP`)BunnyCDN`HTTP_CDN_LOOPCOUNTRY`highnoServer GeoIP`GEOIP_COUNTRY_CODE` (Apache mod\_geoip / nginx ngx\_http\_geoip\_module)mediumnoGeneric`HTTP_X_COUNTRY_CODE`lowno**Signature gating** — for headers that an attacker hitting the origin directly could trivially fake (Cloudflare, Fastly), the resolver verifies that a vendor-set companion header is also present before trusting the country. Country-spoofing alone is rarely a critical attack surface, but cheap to defend against.

**Sentinel filtering** — Cloudflare returns `XX` for unknown geo and `T1` for Tor exits; both are filtered out so a downstream source can take over.

**Shape validation** — only ISO-3166 alpha-2 (2 letters, A-Z) values are returned.

Custom sources
--------------

[](#custom-sources)

Got a local MaxMind GeoLite2 database? Register a callback:

```
use ArrayPress\VisitorCountry\Country;
use ArrayPress\VisitorCountry\Sources\Callback;

Country::add_source(
    new Callback(
        fn( $server ) => MyGeoIP::lookup( $server['REMOTE_ADDR'] ?? '' ),
        'maxmind_local',
        'high'
    )
);
```

`add_source()` appends to the chain (lowest priority). Use `prepend_source()` to take precedence over the bundled CDN sources.

To replace the chain entirely:

```
Country::set_sources( [ new Cloudflare(), new MyCustomSource() ] );
```

To reset to the default chain (useful in tests):

```
Country::reset();
```

Implementing your own Source
----------------------------

[](#implementing-your-own-source)

```
use ArrayPress\VisitorCountry\Source;

class MyMaxMindSource implements Source {
    public function get_name(): string       { return 'maxmind'; }
    public function get_confidence(): string { return 'high'; }

    public function resolve( array $server ): ?string {
        $ip = $server['REMOTE_ADDR'] ?? '';
        if ( $ip === '' ) {
            return null;
        }

        // ... lookup logic ...
        return $country_code; // or null
    }
}
```

If your source reads from a single header with optional signature gating, extend `Sources\HeaderSource` and just declare the header name + signatures + sentinels.

Requirements
------------

[](#requirements)

- PHP 7.4+
- No other dependencies

License
-------

[](#license)

GPL-2.0-or-later.

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance93

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity33

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

34d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/cd6eb8aff0903d87eb674d1ba3c5f3653899c0d7661504eb0deb7798ed86b643?d=identicon)[arraypress](/maintainers/arraypress)

---

Top Contributors

[![arraypress](https://avatars.githubusercontent.com/u/22668877?v=4)](https://github.com/arraypress "arraypress (2 commits)")

---

Tags

geoipgeolocationcloudflarecountrycloudfrontfastlybunnyCDN

### Embed Badge

![Health badge](/badges/arraypress-visitor-country/health.svg)

```
[![Health](https://phpackages.com/badges/arraypress-visitor-country/health.svg)](https://phpackages.com/packages/arraypress-visitor-country)
```

###  Alternatives

[torann/geoip

Support for multiple Geographical Location services.

2.3k14.9M92](/packages/torann-geoip)[ipip/db

IPIP.net officially supported IP database ipdb format parsing library

139207.1k6](/packages/ipip-db)[adrianorosa/laravel-geolocation

Laravel Geo Location package to get details for a given IP Address

6598.6k1](/packages/adrianorosa-laravel-geolocation)[interaction-design-foundation/laravel-geoip

Support for multiple Geographical Location services.

19253.0k3](/packages/interaction-design-foundation-laravel-geoip)[tabgeo/country

geoip (only countries) php library (http://tabgeo.com)

1160.3k](/packages/tabgeo-country)[atchondjo/geoip2country

A lightweight but powerful IP address lookup database solution to determine visitors country

344.5k](/packages/atchondjo-geoip2country)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
