diff --git a/src/Provider/Ip2Geo/.github/workflows/provider.yml b/src/Provider/Ip2Geo/.github/workflows/provider.yml new file mode 100644 index 000000000..7c6069ea1 --- /dev/null +++ b/src/Provider/Ip2Geo/.github/workflows/provider.yml @@ -0,0 +1,33 @@ +name: Provider + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + name: PHP ${{ matrix.php-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: ['8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + steps: + - uses: actions/checkout@v4 + - name: Use PHP ${{ matrix.php-version }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: curl + - name: Validate composer.json and composer.lock + run: composer validate --strict + - name: Install dependencies + run: composer update --prefer-stable --prefer-dist --no-progress + - name: Run test suite + run: composer run-script test-ci + - name: Upload Coverage report + run: | + wget https://scrutinizer-ci.com/ocular.phar + php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml diff --git a/src/Provider/Ip2Geo/.gitignore b/src/Provider/Ip2Geo/.gitignore new file mode 100644 index 000000000..48b8bf907 --- /dev/null +++ b/src/Provider/Ip2Geo/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/src/Provider/Ip2Geo/CHANGELOG.md b/src/Provider/Ip2Geo/CHANGELOG.md new file mode 100644 index 000000000..70c2a8910 --- /dev/null +++ b/src/Provider/Ip2Geo/CHANGELOG.md @@ -0,0 +1,5 @@ +# Change Log + +## 1.0.0 (2026-04-19) + +- Initial release diff --git a/src/Provider/Ip2Geo/Ip2Geo.php b/src/Provider/Ip2Geo/Ip2Geo.php new file mode 100644 index 000000000..13d3b1eba --- /dev/null +++ b/src/Provider/Ip2Geo/Ip2Geo.php @@ -0,0 +1,162 @@ +apiKey = $apiKey; + + parent::__construct($client); + } + + public function geocodeQuery(GeocodeQuery $query): Collection + { + $address = $query->getText(); + + if (!filter_var($address, FILTER_VALIDATE_IP)) { + throw new UnsupportedOperation('The ip2geo provider does not support street addresses, only IP addresses.'); + } + + if (in_array($address, ['127.0.0.1', '::1', '0.0.0.0'], true)) { + return new AddressCollection([Address::createFromArray([])]); + } + + $url = sprintf('%s/convert?ip=%s', self::BASE_URL, $address); + + $request = $this->getRequest($url); + $request = $request->withHeader('X-Api-Key', $this->apiKey); + + $content = $this->getParsedResponse($request); + + $json = json_decode($content, true); + + if (!is_array($json) || !isset($json['success'])) { + throw new InvalidServerResponse(sprintf('Could not decode response from ip2geo for IP "%s".', $address)); + } + + if (true !== $json['success'] || !isset($json['data'])) { + return new AddressCollection([]); + } + + $data = $json['data']; + + return $this->buildResult($data); + } + + public function reverseQuery(ReverseQuery $query): Collection + { + throw new UnsupportedOperation('The ip2geo provider is not able to do reverse geocoding.'); + } + + public function getName(): string + { + return 'ip2geo'; + } + + private function buildResult(array $data): AddressCollection + { + $builder = new AddressBuilder($this->getName()); + + $continent = $data['continent'] ?? []; + $country = $continent['country'] ?? []; + $subdivision = $country['subdivision'] ?? []; + $city = $country['city'] ?? []; + $timezone = $city['timezone'] ?? []; + $flag = $country['flag'] ?? []; + $currency = $country['currency'] ?? []; + $asn = $data['asn'] ?? []; + $registeredCountry = $data['registered_country'] ?? []; + + // Standard geocoder fields + if (isset($city['latitude'], $city['longitude'])) { + $builder->setCoordinates((float) $city['latitude'], (float) $city['longitude']); + } + + if (isset($city['name'])) { + $builder->setLocality($city['name']); + } + + if (isset($city['postal_code'])) { + $builder->setPostalCode($city['postal_code']); + } + + if (isset($subdivision['name'], $subdivision['code'])) { + $builder->addAdminLevel(1, $subdivision['name'], $subdivision['code']); + } + + if (isset($country['name'], $country['code'])) { + $builder->setCountry($country['name']); + $builder->setCountryCode($country['code']); + } + + if (isset($timezone['name'])) { + $builder->setTimezone($timezone['name']); + } + + // Build custom address with extra ip2geo fields + /** @var Ip2GeoAddress $address */ + $address = $builder->build(Ip2GeoAddress::class); + + $address = $address + ->withIp($data['ip'] ?? null) + ->withIpType($data['type'] ?? null) + ->withIsEu($data['is_eu'] ?? null) + ->withContinentName($continent['name'] ?? null) + ->withContinentCode($continent['code'] ?? null) + ->withPhoneCode($country['phone_code'] ?? null) + ->withCapital($country['capital'] ?? null) + ->withTld($country['tld'] ?? null) + ->withFlagEmoji($flag['emoji'] ?? null) + ->withFlagImg($flag['img'] ?? null) + ->withCurrencyName($currency['name'] ?? null) + ->withCurrencyCode($currency['code'] ?? null) + ->withCurrencySymbol($currency['symbol'] ?? null) + ->withGeonameId(isset($city['geoname_id']) ? (int) $city['geoname_id'] : null) + ->withContinentGeonameId(isset($continent['geoname_id']) ? (int) $continent['geoname_id'] : null) + ->withCountryGeonameId(isset($country['geoname_id']) ? (int) $country['geoname_id'] : null) + ->withMetroCode(isset($city['metro_code']) ? (int) $city['metro_code'] : null) + ->withFlagEmojiUnicode($flag['emoji_unicode'] ?? null) + ->withAccuracyRadius(isset($city['accuracy_radius']) ? (int) $city['accuracy_radius'] : null) + ->withTimeNow($timezone['time_now'] ?? null) + ->withAsnNumber(isset($asn['number']) ? (int) $asn['number'] : null) + ->withAsnName($asn['name'] ?? null) + ->withRegisteredCountryName($registeredCountry['name'] ?? null) + ->withRegisteredCountryCode($registeredCountry['code'] ?? null) + ->withRegisteredCountryGeonameId(isset($registeredCountry['geoname_id']) ? (int) $registeredCountry['geoname_id'] : null); + + return new AddressCollection([$address]); + } +} diff --git a/src/Provider/Ip2Geo/Ip2GeoAddress.php b/src/Provider/Ip2Geo/Ip2GeoAddress.php new file mode 100644 index 000000000..80cc19ef7 --- /dev/null +++ b/src/Provider/Ip2Geo/Ip2GeoAddress.php @@ -0,0 +1,400 @@ +ip; + } + + public function withIp(?string $ip): self + { + $new = clone $this; + $new->ip = $ip; + + return $new; + } + + public function getIpType(): ?string + { + return $this->ipType; + } + + public function withIpType(?string $ipType): self + { + $new = clone $this; + $new->ipType = $ipType; + + return $new; + } + + public function isEu(): ?bool + { + return $this->isEu; + } + + public function withIsEu(?bool $isEu): self + { + $new = clone $this; + $new->isEu = $isEu; + + return $new; + } + + public function getContinentName(): ?string + { + return $this->continentName; + } + + public function withContinentName(?string $continentName): self + { + $new = clone $this; + $new->continentName = $continentName; + + return $new; + } + + public function getContinentCode(): ?string + { + return $this->continentCode; + } + + public function withContinentCode(?string $continentCode): self + { + $new = clone $this; + $new->continentCode = $continentCode; + + return $new; + } + + public function getPhoneCode(): ?string + { + return $this->phoneCode; + } + + public function withPhoneCode(?string $phoneCode): self + { + $new = clone $this; + $new->phoneCode = $phoneCode; + + return $new; + } + + public function getCapital(): ?string + { + return $this->capital; + } + + public function withCapital(?string $capital): self + { + $new = clone $this; + $new->capital = $capital; + + return $new; + } + + public function getTld(): ?string + { + return $this->tld; + } + + public function withTld(?string $tld): self + { + $new = clone $this; + $new->tld = $tld; + + return $new; + } + + public function getFlagEmoji(): ?string + { + return $this->flagEmoji; + } + + public function withFlagEmoji(?string $flagEmoji): self + { + $new = clone $this; + $new->flagEmoji = $flagEmoji; + + return $new; + } + + public function getFlagImg(): ?string + { + return $this->flagImg; + } + + public function withFlagImg(?string $flagImg): self + { + $new = clone $this; + $new->flagImg = $flagImg; + + return $new; + } + + public function getCurrencyName(): ?string + { + return $this->currencyName; + } + + public function withCurrencyName(?string $currencyName): self + { + $new = clone $this; + $new->currencyName = $currencyName; + + return $new; + } + + public function getCurrencyCode(): ?string + { + return $this->currencyCode; + } + + public function withCurrencyCode(?string $currencyCode): self + { + $new = clone $this; + $new->currencyCode = $currencyCode; + + return $new; + } + + public function getCurrencySymbol(): ?string + { + return $this->currencySymbol; + } + + public function withCurrencySymbol(?string $currencySymbol): self + { + $new = clone $this; + $new->currencySymbol = $currencySymbol; + + return $new; + } + + public function getGeonameId(): ?int + { + return $this->geonameId; + } + + public function withGeonameId(?int $geonameId): self + { + $new = clone $this; + $new->geonameId = $geonameId; + + return $new; + } + + public function getContinentGeonameId(): ?int + { + return $this->continentGeonameId; + } + + public function withContinentGeonameId(?int $continentGeonameId): self + { + $new = clone $this; + $new->continentGeonameId = $continentGeonameId; + + return $new; + } + + public function getCountryGeonameId(): ?int + { + return $this->countryGeonameId; + } + + public function withCountryGeonameId(?int $countryGeonameId): self + { + $new = clone $this; + $new->countryGeonameId = $countryGeonameId; + + return $new; + } + + public function getMetroCode(): ?int + { + return $this->metroCode; + } + + public function withMetroCode(?int $metroCode): self + { + $new = clone $this; + $new->metroCode = $metroCode; + + return $new; + } + + public function getFlagEmojiUnicode(): ?string + { + return $this->flagEmojiUnicode; + } + + public function withFlagEmojiUnicode(?string $flagEmojiUnicode): self + { + $new = clone $this; + $new->flagEmojiUnicode = $flagEmojiUnicode; + + return $new; + } + + public function getRegisteredCountryGeonameId(): ?int + { + return $this->registeredCountryGeonameId; + } + + public function withRegisteredCountryGeonameId(?int $registeredCountryGeonameId): self + { + $new = clone $this; + $new->registeredCountryGeonameId = $registeredCountryGeonameId; + + return $new; + } + + public function getAccuracyRadius(): ?int + { + return $this->accuracyRadius; + } + + public function withAccuracyRadius(?int $accuracyRadius): self + { + $new = clone $this; + $new->accuracyRadius = $accuracyRadius; + + return $new; + } + + public function getTimeNow(): ?string + { + return $this->timeNow; + } + + public function withTimeNow(?string $timeNow): self + { + $new = clone $this; + $new->timeNow = $timeNow; + + return $new; + } + + public function getAsnNumber(): ?int + { + return $this->asnNumber; + } + + public function withAsnNumber(?int $asnNumber): self + { + $new = clone $this; + $new->asnNumber = $asnNumber; + + return $new; + } + + public function getAsnName(): ?string + { + return $this->asnName; + } + + public function withAsnName(?string $asnName): self + { + $new = clone $this; + $new->asnName = $asnName; + + return $new; + } + + public function getRegisteredCountryName(): ?string + { + return $this->registeredCountryName; + } + + public function withRegisteredCountryName(?string $registeredCountryName): self + { + $new = clone $this; + $new->registeredCountryName = $registeredCountryName; + + return $new; + } + + public function getRegisteredCountryCode(): ?string + { + return $this->registeredCountryCode; + } + + public function withRegisteredCountryCode(?string $registeredCountryCode): self + { + $new = clone $this; + $new->registeredCountryCode = $registeredCountryCode; + + return $new; + } +} diff --git a/src/Provider/Ip2Geo/LICENSE b/src/Provider/Ip2Geo/LICENSE new file mode 100644 index 000000000..8aa8246ef --- /dev/null +++ b/src/Provider/Ip2Geo/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2011 — William Durand + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/Provider/Ip2Geo/README.md b/src/Provider/Ip2Geo/README.md new file mode 100644 index 000000000..d5441aafe --- /dev/null +++ b/src/Provider/Ip2Geo/README.md @@ -0,0 +1,113 @@ +# ip2geo Provider for Geocoder PHP + +[![Latest Stable Version](https://poser.pugx.org/geocoder-php/ip2geo-provider/v/stable)](https://packagist.org/packages/geocoder-php/ip2geo-provider) +[![License](https://poser.pugx.org/geocoder-php/ip2geo-provider/license)](https://packagist.org/packages/geocoder-php/ip2geo-provider) + +This is the [ip2geo](https://ip2geo.dev) provider for the [Geocoder PHP](https://github.com/geocoder-php/Geocoder) library. + +## Installation + +```bash +composer require geocoder-php/ip2geo-provider +``` + +## Usage + +An API key is required. You can obtain one at [ip2geo.dev](https://ip2geo.dev). + +```php +use Geocoder\Provider\Ip2Geo\Ip2Geo; +use Geocoder\Provider\Ip2Geo\Ip2GeoAddress; +use Geocoder\Query\GeocodeQuery; +use Http\Discovery\Psr18ClientDiscovery; + +$httpClient = Psr18ClientDiscovery::find(); +$provider = new Ip2Geo($httpClient, 'your-api-key'); + +$results = $provider->geocodeQuery(GeocodeQuery::create('8.8.8.8')); + +/** @var Ip2GeoAddress $address */ +$address = $results->first(); +``` + +### Standard Geocoder Fields + +These fields are available on all Geocoder Address objects: + +```php +echo $address->getLocality(); // "Mountain View" +echo $address->getCountry(); // "United States" +echo $address->getCountryCode(); // "US" +echo $address->getTimezone(); // "America/Los_Angeles" +echo $address->getPostalCode(); // "94035" + +$coordinates = $address->getCoordinates(); +echo $coordinates->getLatitude(); // 37.386 +echo $coordinates->getLongitude(); // -122.0838 + +$adminLevels = $address->getAdminLevels(); +echo $adminLevels->get(1)->getName(); // "California" +echo $adminLevels->get(1)->getCode(); // "CA" +``` + +### Extra ip2geo Fields + +The returned `Ip2GeoAddress` object extends the standard `Address` with additional +getters for all fields provided by the ip2geo API: + +```php +// IP info +echo $address->getIp(); // "8.8.8.8" +echo $address->getIpType(); // "IPv4" +echo $address->isEu(); // false + +// Continent +echo $address->getContinentName(); // "North America" +echo $address->getContinentCode(); // "NA" + +// Country extras +echo $address->getPhoneCode(); // "+1" +echo $address->getCapital(); // "Washington D.C." +echo $address->getTld(); // ".us" + +// Flag +echo $address->getFlagEmoji(); // (flag emoji) +echo $address->getFlagImg(); // "https://flagcdn.com/us.svg" + +// Currency +echo $address->getCurrencyName(); // "United States Dollar" +echo $address->getCurrencyCode(); // "USD" +echo $address->getCurrencySymbol(); // "$" + +// City extras +echo $address->getGeonameId(); // 5375480 +echo $address->getAccuracyRadius(); // 1000 +echo $address->getTimeNow(); // "2026-04-05T10:30:00-07:00" + +// ASN +echo $address->getAsnNumber(); // 15169 +echo $address->getAsnName(); // "Google LLC" + +// Registered country +echo $address->getRegisteredCountryName(); // "United States" +echo $address->getRegisteredCountryCode(); // "US" +``` + +## Supported Operations + +| Operation | Supported | +|------------------|-----------| +| Geocode (IP) | Yes | +| Geocode (Street) | No | +| Reverse | No | + +## Running Tests + +```bash +composer install +vendor/bin/phpunit +``` + +## License + +This package is licensed under the [MIT License](LICENSE). diff --git a/src/Provider/Ip2Geo/Tests/.cached_responses/.gitkeep b/src/Provider/Ip2Geo/Tests/.cached_responses/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/Provider/Ip2Geo/Tests/IntegrationTest.php b/src/Provider/Ip2Geo/Tests/IntegrationTest.php new file mode 100644 index 000000000..f665f50cc --- /dev/null +++ b/src/Provider/Ip2Geo/Tests/IntegrationTest.php @@ -0,0 +1,47 @@ +getApiKey()); + } + + protected function getCacheDir(): string + { + return __DIR__.'/.cached_responses'; + } + + protected function getApiKey(): string + { + if (!isset($_SERVER['IP2GEO_API_KEY'])) { + $this->markTestSkipped('No ip2geo API key'); + } + + return $_SERVER['IP2GEO_API_KEY']; + } +} diff --git a/src/Provider/Ip2Geo/Tests/Ip2GeoTest.php b/src/Provider/Ip2Geo/Tests/Ip2GeoTest.php new file mode 100644 index 000000000..391cd7ec3 --- /dev/null +++ b/src/Provider/Ip2Geo/Tests/Ip2GeoTest.php @@ -0,0 +1,258 @@ +getMockedHttpClient(), 'api-key'); + + $this->assertSame('ip2geo', $provider->getName()); + } + + public function testEmptyApiKeyThrows(): void + { + $this->expectException(InvalidCredentials::class); + $this->expectExceptionMessage('An API key is required.'); + + new Ip2Geo($this->getMockedHttpClient(), ''); + } + + public function testGeocodeWithStreetAddress(): void + { + $this->expectException(UnsupportedOperation::class); + $this->expectExceptionMessage('The ip2geo provider does not support street addresses, only IP addresses.'); + + $provider = new Ip2Geo($this->getMockedHttpClient(), 'api-key'); + $provider->geocodeQuery(GeocodeQuery::create('123 Main Street')); + } + + public function testReverseQuery(): void + { + $this->expectException(UnsupportedOperation::class); + $this->expectExceptionMessage('The ip2geo provider is not able to do reverse geocoding.'); + + $provider = new Ip2Geo($this->getMockedHttpClient(), 'api-key'); + $provider->reverseQuery(ReverseQuery::fromCoordinates(37.386, -122.0838)); + } + + public function testGeocodeWithLocalhostIpv4(): void + { + $provider = new Ip2Geo($this->getMockedHttpClient(), 'api-key'); + $results = $provider->geocodeQuery(GeocodeQuery::create('127.0.0.1')); + + $this->assertInstanceOf('Geocoder\Model\AddressCollection', $results); + $this->assertCount(1, $results); + + $result = $results->first(); + $this->assertNull($result->getLocality()); + $this->assertNull($result->getCoordinates()); + } + + public function testGeocodeWithLocalhostIpv6(): void + { + $provider = new Ip2Geo($this->getMockedHttpClient(), 'api-key'); + $results = $provider->geocodeQuery(GeocodeQuery::create('::1')); + + $this->assertInstanceOf('Geocoder\Model\AddressCollection', $results); + $this->assertCount(1, $results); + } + + public function testGeocodeWithInvalidResponse(): void + { + $this->expectException(InvalidServerResponse::class); + + $provider = new Ip2Geo($this->getMockedHttpClient('not-json'), 'api-key'); + $provider->geocodeQuery(GeocodeQuery::create('8.8.8.8')); + } + + public function testGeocodeWithUnsuccessfulResponse(): void + { + $body = json_encode([ + 'success' => false, + 'message' => 'Invalid API key', + ]); + + $provider = new Ip2Geo($this->getMockedHttpClient($body), 'invalid-key'); + $results = $provider->geocodeQuery(GeocodeQuery::create('8.8.8.8')); + + $this->assertEmpty($results); + } + + public function testGeocodeWithRealIp(): void + { + $body = json_encode($this->getSuccessResponse()); + + $provider = new Ip2Geo($this->getMockedHttpClient($body), 'api-key'); + $results = $provider->geocodeQuery(GeocodeQuery::create('8.8.8.8')); + + $this->assertCount(1, $results); + + /** @var Ip2GeoAddress $result */ + $result = $results->first(); + + $this->assertInstanceOf(Ip2GeoAddress::class, $result); + + // Standard geocoder fields + $this->assertSame('ip2geo', $result->getProvidedBy()); + $this->assertSame('Mountain View', $result->getLocality()); + $this->assertSame('94035', $result->getPostalCode()); + $this->assertEqualsWithDelta(37.386, $result->getCoordinates()->getLatitude(), 0.001); + $this->assertEqualsWithDelta(-122.0838, $result->getCoordinates()->getLongitude(), 0.001); + $this->assertSame('United States', $result->getCountry()->getName()); + $this->assertSame('US', $result->getCountry()->getCode()); + $this->assertSame('California', $result->getAdminLevels()->get(1)->getName()); + $this->assertSame('CA', $result->getAdminLevels()->get(1)->getCode()); + $this->assertSame('America/Los_Angeles', $result->getTimezone()); + + // Extra ip2geo fields + $this->assertSame('8.8.8.8', $result->getIp()); + $this->assertSame('IPv4', $result->getIpType()); + $this->assertFalse($result->isEu()); + $this->assertSame('North America', $result->getContinentName()); + $this->assertSame('NA', $result->getContinentCode()); + $this->assertSame('+1', $result->getPhoneCode()); + $this->assertSame('Washington D.C.', $result->getCapital()); + $this->assertSame('.us', $result->getTld()); + $this->assertSame('https://flagcdn.com/us.svg', $result->getFlagImg()); + $this->assertSame('USD', $result->getCurrencyCode()); + $this->assertSame('United States Dollar', $result->getCurrencyName()); + $this->assertSame('$', $result->getCurrencySymbol()); + $this->assertSame(5375480, $result->getGeonameId()); + $this->assertSame(6255149, $result->getContinentGeonameId()); + $this->assertSame(6252001, $result->getCountryGeonameId()); + $this->assertSame(807, $result->getMetroCode()); + $this->assertSame('U+1F1FA U+1F1F8', $result->getFlagEmojiUnicode()); + $this->assertSame(6252001, $result->getRegisteredCountryGeonameId()); + $this->assertSame(1000, $result->getAccuracyRadius()); + $this->assertSame('2026-04-05T10:30:00-07:00', $result->getTimeNow()); + $this->assertSame(15169, $result->getAsnNumber()); + $this->assertSame('Google LLC', $result->getAsnName()); + $this->assertSame('United States', $result->getRegisteredCountryName()); + $this->assertSame('US', $result->getRegisteredCountryCode()); + } + + public function testGeocodeWithMinimalData(): void + { + $body = json_encode([ + 'success' => true, + 'data' => [ + 'ip' => '1.1.1.1', + 'type' => 'IPv4', + 'continent' => [ + 'name' => 'Oceania', + 'code' => 'OC', + 'country' => [ + 'name' => 'Australia', + 'code' => 'AU', + ], + ], + ], + ]); + + $provider = new Ip2Geo($this->getMockedHttpClient($body), 'api-key'); + $results = $provider->geocodeQuery(GeocodeQuery::create('1.1.1.1')); + + /** @var Ip2GeoAddress $result */ + $result = $results->first(); + + $this->assertInstanceOf(Ip2GeoAddress::class, $result); + $this->assertSame('1.1.1.1', $result->getIp()); + $this->assertSame('Australia', $result->getCountry()->getName()); + $this->assertSame('AU', $result->getCountry()->getCode()); + $this->assertNull($result->getLocality()); + $this->assertNull($result->getCoordinates()); + $this->assertNull($result->getAsnNumber()); + $this->assertNull($result->getCurrencyCode()); + $this->assertNull($result->getFlagEmoji()); + } + + /** + * Returns a full successful API response for testing. + */ + private function getSuccessResponse(): array + { + return [ + 'success' => true, + 'data' => [ + 'ip' => '8.8.8.8', + 'type' => 'IPv4', + 'is_eu' => false, + 'continent' => [ + 'name' => 'North America', + 'code' => 'NA', + 'geoname_id' => 6255149, + 'country' => [ + 'name' => 'United States', + 'code' => 'US', + 'geoname_id' => 6252001, + 'phone_code' => '+1', + 'capital' => 'Washington D.C.', + 'tld' => '.us', + 'flag' => [ + 'emoji' => "\u{1F1FA}\u{1F1F8}", + 'emoji_unicode' => 'U+1F1FA U+1F1F8', + 'img' => 'https://flagcdn.com/us.svg', + ], + 'currency' => [ + 'name' => 'United States Dollar', + 'code' => 'USD', + 'symbol' => '$', + ], + 'subdivision' => [ + 'name' => 'California', + 'code' => 'CA', + ], + 'city' => [ + 'name' => 'Mountain View', + 'latitude' => 37.386, + 'longitude' => -122.0838, + 'postal_code' => '94035', + 'geoname_id' => 5375480, + 'metro_code' => 807, + 'accuracy_radius' => 1000, + 'timezone' => [ + 'name' => 'America/Los_Angeles', + 'time_now' => '2026-04-05T10:30:00-07:00', + ], + ], + ], + ], + 'asn' => [ + 'number' => 15169, + 'name' => 'Google LLC', + ], + 'registered_country' => [ + 'name' => 'United States', + 'code' => 'US', + 'geoname_id' => 6252001, + ], + ], + ]; + } +} diff --git a/src/Provider/Ip2Geo/composer.json b/src/Provider/Ip2Geo/composer.json new file mode 100644 index 000000000..7d82f22d8 --- /dev/null +++ b/src/Provider/Ip2Geo/composer.json @@ -0,0 +1,46 @@ +{ + "name": "geocoder-php/ip2geo-provider", + "type": "library", + "description": "Geocoder ip2geo adapter", + "keywords": ["geocoder", "geocoding", "ip", "geolocation", "ip2geo"], + "homepage": "https://ip2geo.dev", + "license": "MIT", + "authors": [ + { + "name": "ip2geo", + "email": "support@ip2geo.dev" + } + ], + "require": { + "php": "^8.0", + "geocoder-php/common-http": "^4.0", + "willdurand/geocoder": "^4.0|^5.0" + }, + "provide": { + "geocoder-php/provider-implementation": "1.0" + }, + "require-dev": { + "geocoder-php/provider-integration-tests": "^1.6.3", + "php-http/message": "^1.0", + "phpunit/phpunit": "^9.6.11" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Geocoder\\Provider\\Ip2Geo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "prefer-stable": true, + "scripts": { + "test": "vendor/bin/phpunit", + "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml" + } +} diff --git a/src/Provider/Ip2Geo/phpunit.xml.dist b/src/Provider/Ip2Geo/phpunit.xml.dist new file mode 100644 index 000000000..f095277ce --- /dev/null +++ b/src/Provider/Ip2Geo/phpunit.xml.dist @@ -0,0 +1,14 @@ + + + + + Tests + + +