diff --git a/.github/workflows/provider.yml b/.github/workflows/provider.yml
index ea7b27fa7..82c747902 100644
--- a/.github/workflows/provider.yml
+++ b/.github/workflows/provider.yml
@@ -34,6 +34,7 @@ jobs:
- HostIp
- IP2Location
# - IP2LocationBinary
+ - IPGeolocation
- IpInfo
- IpInfoDb
- Ipstack
diff --git a/.github/workflows/subtree.yml b/.github/workflows/subtree.yml
index 577069128..322d09b49 100644
--- a/.github/workflows/subtree.yml
+++ b/.github/workflows/subtree.yml
@@ -101,6 +101,7 @@ jobs:
- { folder: HostIp, repository: host-ip-provider }
- { folder: IP2Location, repository: ip2location-provider }
- { folder: IP2LocationBinary, repository: ip2location-binary-provider }
+ - { folder: IPGeolocation, repository: ipgeolocation-provider }
- { folder: IpInfo, repository: ip-info-provider }
- { folder: IpInfoDb, repository: ip-info-db-provider }
- { folder: Ipstack, repository: ipstack-provider }
diff --git a/README.md b/README.md
index 31b250196..fe6212fee 100644
--- a/README.md
+++ b/README.md
@@ -171,6 +171,7 @@ Provider | Package | Features | Stats
[HostIp](https://github.com/geocoder-php/host-ip-provider) | `geocoder-php/host-ip-provider` | IPv4
[Website](http://www.hostip.info/use.html) | [](https://packagist.org/packages/geocoder-php/host-ip-provider)
[](https://packagist.org/packages/geocoder-php/host-ip-provider)
[IP2Location](https://github.com/geocoder-php/ip2location-provider) | `geocoder-php/ip2location-provider` | IPv4, IPv6
[Website](https://www.ip2location.com/) | [](https://packagist.org/packages/geocoder-php/ip2location-provider)
[](https://packagist.org/packages/geocoder-php/ip2location-provider)
[IP2Location Binary](https://github.com/geocoder-php/ip2location-binary-provider) | `geocoder-php/ip2location-binary-provider` | IPv4, IPv6
[Website](https://www.ip2location.com/) | [](https://packagist.org/packages/geocoder-php/ip2location-binary-provider)
[](https://packagist.org/packages/geocoder-php/ip2location-binary-provider)
+[IPGeolocation](https://github.com/geocoder-php/ipgeolocation-provider) | `geocoder-php/ipgeolocation-provider` | IPv4, IPv6
[Website](https://ipgeolocation.io/) | [](https://packagist.org/packages/geocoder-php/ipgeolocation-provider)
[](https://packagist.org/packages/geocoder-php/ipgeolocation-provider)
[IpInfo](https://github.com/geocoder-php/ip-info-provider) | `geocoder-php/ip-info-provider` | IPv4, IPv6
[Website](https://ipinfo.io/) | [](https://packagist.org/packages/geocoder-php/ip-info-provider)
[](https://packagist.org/packages/geocoder-php/ip-info-provider)
[IpInfoDB](https://github.com/geocoder-php/ip-info-db-provider) | `geocoder-php/ip-info-db-provider` | IPv4
[Website](http://ipinfodb.com/) | [](https://packagist.org/packages/geocoder-php/ip-info-db-provider)
[](https://packagist.org/packages/geocoder-php/ip-info-db-provider)
[ipstack](https://github.com/geocoder-php/ipstack-provider) | `geocoder-php/ipstack-provider` | IPv4, IPv6
[Website](https://ipstack.com/) | [](https://packagist.org/packages/geocoder-php/ipstack-provider)
[](https://packagist.org/packages/geocoder-php/ipstack-provider)
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index d01fae0f5..a886184a5 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -42,6 +42,7 @@
+
diff --git a/src/Provider/IPGeolocation/.gitattributes b/src/Provider/IPGeolocation/.gitattributes
new file mode 100644
index 000000000..d04504afd
--- /dev/null
+++ b/src/Provider/IPGeolocation/.gitattributes
@@ -0,0 +1,4 @@
+.gitattributes export-ignore
+.travis.yml export-ignore
+phpunit.xml.dist export-ignore
+Tests/ export-ignore
diff --git a/src/Provider/IPGeolocation/.github/workflows/provider.yml b/src/Provider/IPGeolocation/.github/workflows/provider.yml
new file mode 100644
index 000000000..7c6069ea1
--- /dev/null
+++ b/src/Provider/IPGeolocation/.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/IPGeolocation/.gitignore b/src/Provider/IPGeolocation/.gitignore
new file mode 100644
index 000000000..c49a5d8df
--- /dev/null
+++ b/src/Provider/IPGeolocation/.gitignore
@@ -0,0 +1,3 @@
+vendor/
+composer.lock
+phpunit.xml
diff --git a/src/Provider/IPGeolocation/CHANGELOG.md b/src/Provider/IPGeolocation/CHANGELOG.md
new file mode 100644
index 000000000..746b7151f
--- /dev/null
+++ b/src/Provider/IPGeolocation/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Change Log
+
+The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release.
+
+## 1.0.0
+
+First release of this library.
diff --git a/src/Provider/IPGeolocation/IPGeolocation.php b/src/Provider/IPGeolocation/IPGeolocation.php
new file mode 100644
index 000000000..eb8183300
--- /dev/null
+++ b/src/Provider/IPGeolocation/IPGeolocation.php
@@ -0,0 +1,152 @@
+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 IPGeolocation provider does not support street addresses, only IP addresses.');
+ }
+
+ if (in_array($address, ['127.0.0.1', '::1'], true)) {
+ return new AddressCollection([$this->getLocationForLocalhost()]);
+ }
+
+ $params = [
+ 'apiKey' => $this->apiKey,
+ 'ip' => $address,
+ 'fields' => 'location,time_zone.name',
+ ];
+
+ if (null !== $query->getLocale()) {
+ $params['lang'] = $query->getLocale();
+ }
+
+ return $this->executeQuery(self::GEOCODE_ENDPOINT_URL.'?'.http_build_query($params));
+ }
+
+ public function reverseQuery(ReverseQuery $query): Collection
+ {
+ throw new UnsupportedOperation('The IPGeolocation provider is not able to do reverse geocoding.');
+ }
+
+ public function getName(): string
+ {
+ return 'ipgeolocation';
+ }
+
+ private function executeQuery(string $url): AddressCollection
+ {
+ $response = $this->getHttpClient()->sendRequest($this->getRequest($url));
+ $statusCode = $response->getStatusCode();
+
+ // 404: the IP is not in the database, 423: the IP is private or reserved
+ if (404 === $statusCode || 423 === $statusCode) {
+ return new AddressCollection([]);
+ }
+
+ if (401 === $statusCode || 403 === $statusCode) {
+ throw new InvalidCredentials();
+ } elseif (429 === $statusCode) {
+ throw new QuotaExceeded();
+ } elseif ($statusCode >= 300) {
+ throw InvalidServerResponse::create($url, $statusCode);
+ }
+
+ $body = (string) $response->getBody();
+ if ('' === $body) {
+ throw InvalidServerResponse::emptyResponse($url);
+ }
+
+ $data = json_decode($body, true);
+ if (!is_array($data)) {
+ throw InvalidServerResponse::create($url, $statusCode);
+ }
+
+ if (!isset($data['location']) || !is_array($data['location'])) {
+ return new AddressCollection([]);
+ }
+
+ $location = array_map(fn ($value) => '' === $value ? null : $value, $data['location']);
+
+ $adminLevels = [];
+ if (isset($location['state_prov'])) {
+ $adminLevels[] = [
+ 'name' => $location['state_prov'],
+ 'code' => $location['state_code'] ?? null,
+ 'level' => 1,
+ ];
+ }
+ if (isset($location['district'])) {
+ $adminLevels[] = ['name' => $location['district'], 'level' => 2];
+ }
+
+ return new AddressCollection([
+ Address::createFromArray([
+ 'providedBy' => $this->getName(),
+ 'latitude' => isset($location['latitude']) ? (float) $location['latitude'] : null,
+ 'longitude' => isset($location['longitude']) ? (float) $location['longitude'] : null,
+ 'locality' => $location['city'] ?? null,
+ 'postalCode' => $location['zipcode'] ?? null,
+ 'adminLevels' => $adminLevels,
+ 'country' => $location['country_name'] ?? null,
+ 'countryCode' => $location['country_code2'] ?? null,
+ 'timezone' => $data['time_zone']['name'] ?? null,
+ ]),
+ ]);
+ }
+}
diff --git a/src/Provider/IPGeolocation/LICENSE b/src/Provider/IPGeolocation/LICENSE
new file mode 100644
index 000000000..8aa8246ef
--- /dev/null
+++ b/src/Provider/IPGeolocation/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/IPGeolocation/Readme.md b/src/Provider/IPGeolocation/Readme.md
new file mode 100644
index 000000000..d658fa84b
--- /dev/null
+++ b/src/Provider/IPGeolocation/Readme.md
@@ -0,0 +1,39 @@
+# IPGeolocation.io Geocoder provider
+[](https://packagist.org/packages/geocoder-php/ipgeolocation-provider)
+[](https://packagist.org/packages/geocoder-php/ipgeolocation-provider)
+[](https://packagist.org/packages/geocoder-php/ipgeolocation-provider)
+[](LICENSE)
+
+This is the [IPGeolocation.io](https://ipgeolocation.io/) provider from the PHP Geocoder. This is a **READ ONLY** repository. See the
+[main repo](https://github.com/geocoder-php/Geocoder) for information and documentation.
+
+### Install
+
+```bash
+composer require geocoder-php/ipgeolocation-provider
+```
+
+### Usage
+
+The provider geocodes IPv4 and IPv6 addresses. It does not support street addresses or reverse geocoding.
+
+```php
+$httpClient = new \Http\Discovery\Psr18Client();
+$provider = new \Geocoder\Provider\IPGeolocation\IPGeolocation($httpClient, 'your-api-key');
+
+$result = $provider->geocodeQuery(\Geocoder\Query\GeocodeQuery::create('8.8.8.8'));
+```
+
+Sign up for a free API key at [app.ipgeolocation.io](https://app.ipgeolocation.io/login).
+
+### Note
+
+Results are returned in English by default. Pass a locale with `GeocodeQuery::withLocale()` to request another
+language; languages other than English need a paid plan.
+
+IP addresses that are private, reserved or missing from the IPGeolocation.io database return an empty collection.
+
+### Contribute
+
+Contributions are very welcome! Send a pull request to the [main repository](https://github.com/geocoder-php/Geocoder) or
+report any issues you find on the [issue tracker](https://github.com/geocoder-php/Geocoder/issues).
diff --git a/src/Provider/IPGeolocation/Tests/.cached_responses/api.ipgeolocation.io_3b1b489ab7d6998edb0e17ed3cf573a42f1d8761 b/src/Provider/IPGeolocation/Tests/.cached_responses/api.ipgeolocation.io_3b1b489ab7d6998edb0e17ed3cf573a42f1d8761
new file mode 100644
index 000000000..65d9142d1
--- /dev/null
+++ b/src/Provider/IPGeolocation/Tests/.cached_responses/api.ipgeolocation.io_3b1b489ab7d6998edb0e17ed3cf573a42f1d8761
@@ -0,0 +1 @@
+s:562:"{"ip":"83.227.123.8","location":{"continent_code":"EU","continent_name":"Europe","country_code2":"SE","country_code3":"SWE","country_name":"Sweden","country_name_official":"Kingdom of Sweden","country_capital":"Stockholm","state_prov":"Stockholm County","state_code":"SE-AB","district":"Stockholm","city":"Solna","zipcode":"169 03","latitude":"59.36888","longitude":"18.00843","is_eu":true,"country_flag":"https://ipgeolocation.io/static/flags/se_64.png","geoname_id":"8128160","country_emoji":"\uD83C\uDDF8\uD83C\uDDEA"},"time_zone":{"name":"Europe/Stockholm"}}";
\ No newline at end of file
diff --git a/src/Provider/IPGeolocation/Tests/.cached_responses/api.ipgeolocation.io_a328c24e92ee2cf0b70e454939e54f43d844a141 b/src/Provider/IPGeolocation/Tests/.cached_responses/api.ipgeolocation.io_a328c24e92ee2cf0b70e454939e54f43d844a141
new file mode 100644
index 000000000..ad3f6c741
--- /dev/null
+++ b/src/Provider/IPGeolocation/Tests/.cached_responses/api.ipgeolocation.io_a328c24e92ee2cf0b70e454939e54f43d844a141
@@ -0,0 +1 @@
+s:68:"{"message":"'2001:db8:0:42:0:8a2e:370:7334' is a bogon IP address."}";
\ No newline at end of file
diff --git a/src/Provider/IPGeolocation/Tests/IPGeolocationTest.php b/src/Provider/IPGeolocation/Tests/IPGeolocationTest.php
new file mode 100644
index 000000000..3831198d9
--- /dev/null
+++ b/src/Provider/IPGeolocation/Tests/IPGeolocationTest.php
@@ -0,0 +1,234 @@
+getMockedHttpClient(), 'api_key');
+ $this->assertEquals('ipgeolocation', $provider->getName());
+ }
+
+ public function testMissingApiKey(): void
+ {
+ $this->expectException(\Geocoder\Exception\InvalidCredentials::class);
+ $this->expectExceptionMessage('No API key provided.');
+
+ new IPGeolocation($this->getMockedHttpClient(), '');
+ }
+
+ public function testGeocodeWithAddress(): void
+ {
+ $this->expectException(\Geocoder\Exception\UnsupportedOperation::class);
+ $this->expectExceptionMessage('The IPGeolocation provider does not support street addresses, only IP addresses.');
+
+ $provider = new IPGeolocation($this->getMockedHttpClient(), 'api_key');
+ $provider->geocodeQuery(GeocodeQuery::create('10 avenue Gambetta, Paris, France'));
+ }
+
+ /** @dataProvider provideLocalhostIps */
+ public function testGeocodeWithLocalhost(string $localhostIp): void
+ {
+ $provider = new IPGeolocation($this->getMockedHttpClient(), 'api_key');
+ $results = $provider->geocodeQuery(GeocodeQuery::create($localhostIp));
+
+ $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $results);
+ $this->assertCount(1, $results);
+
+ /** @var Location $result */
+ $result = $results->first();
+ $this->assertEquals('localhost', $result->getLocality());
+ $this->assertEquals('localhost', $result->getCountry()->getName());
+ }
+
+ /**
+ * @return iterable
+ */
+ public function provideLocalhostIps(): iterable
+ {
+ yield ['127.0.0.1'];
+ yield ['::1'];
+ }
+
+ public function testRequestUrl(): void
+ {
+ $provider = new IPGeolocation($this->getMockedHttpClientCallback(
+ function (RequestInterface $request): ResponseInterface {
+ $this->assertEquals('GET', $request->getMethod());
+ $this->assertEquals(
+ 'https://api.ipgeolocation.io/v3/ipgeo?apiKey=api_key&ip=165.227.0.0&fields=location%2Ctime_zone.name&lang=fr',
+ (string) $request->getUri()
+ );
+
+ return $this->getResponse(200, $this->getFreePlanBody());
+ }
+ ), 'api_key');
+
+ $provider->geocodeQuery(GeocodeQuery::create('165.227.0.0')->withLocale('fr'));
+ }
+
+ public function testGeocodeWithIPv4(): void
+ {
+ $provider = new IPGeolocation($this->getMockedHttpClient($this->getFreePlanBody()), 'api_key');
+ $results = $provider->geocodeQuery(GeocodeQuery::create('165.227.0.0'));
+
+ $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $results);
+ $this->assertCount(1, $results);
+
+ /** @var Location $result */
+ $result = $results->first();
+ $this->assertInstanceOf(\Geocoder\Model\Address::class, $result);
+ $this->assertEqualsWithDelta(37.35983, $result->getCoordinates()->getLatitude(), 0.00001);
+ $this->assertEqualsWithDelta(-121.98144, $result->getCoordinates()->getLongitude(), 0.00001);
+ $this->assertEquals('Santa Clara', $result->getLocality());
+ $this->assertEquals('95051', $result->getPostalCode());
+ $this->assertCount(2, $result->getAdminLevels());
+ $this->assertEquals('California', $result->getAdminLevels()->get(1)->getName());
+ $this->assertEquals('US-CA', $result->getAdminLevels()->get(1)->getCode());
+ $this->assertEquals('Santa Clara County', $result->getAdminLevels()->get(2)->getName());
+ $this->assertEquals('United States', $result->getCountry()->getName());
+ $this->assertEquals('US', $result->getCountry()->getCode());
+ $this->assertEquals('America/Los_Angeles', $result->getTimezone());
+ $this->assertEquals('ipgeolocation', $result->getProvidedBy());
+ }
+
+ public function testGeocodeWithEmptyFields(): void
+ {
+ $body = json_encode([
+ 'ip' => '2001:4860:4860::8888',
+ 'location' => [
+ 'country_code2' => 'US',
+ 'country_name' => 'United States',
+ 'state_prov' => '',
+ 'state_code' => '',
+ 'district' => '',
+ 'city' => '',
+ 'zipcode' => '',
+ 'latitude' => '37.75100',
+ 'longitude' => '-97.82200',
+ ],
+ ]);
+
+ $provider = new IPGeolocation($this->getMockedHttpClient($body), 'api_key');
+ $results = $provider->geocodeQuery(GeocodeQuery::create('2001:4860:4860::8888'));
+
+ /** @var Location $result */
+ $result = $results->first();
+ $this->assertNull($result->getLocality());
+ $this->assertNull($result->getPostalCode());
+ $this->assertNull($result->getTimezone());
+ $this->assertCount(0, $result->getAdminLevels());
+ $this->assertEquals('US', $result->getCountry()->getCode());
+ }
+
+ /** @dataProvider provideNoResultStatusCodes */
+ public function testGeocodeWithNoResult(int $statusCode, string $message): void
+ {
+ $provider = new IPGeolocation(
+ $this->getMockedHttpClient(json_encode(['message' => $message]), $statusCode),
+ 'api_key'
+ );
+
+ $results = $provider->geocodeQuery(GeocodeQuery::create('10.0.0.1'));
+
+ $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $results);
+ $this->assertCount(0, $results);
+ }
+
+ /**
+ * @return iterable
+ */
+ public function provideNoResultStatusCodes(): iterable
+ {
+ yield [404, 'Provided IPv4 or IPv6 address does not exist in our database.'];
+ yield [423, "'10.0.0.1' is a bogon IP address."];
+ }
+
+ public function testInvalidApiKey(): void
+ {
+ $this->expectException(\Geocoder\Exception\InvalidCredentials::class);
+
+ $body = json_encode(['message' => 'Provided API key is not valid. Contact technical support for assistance at support@ipgeolocation.io']);
+ $provider = new IPGeolocation($this->getMockedHttpClient($body, 401), 'api_key');
+ $provider->geocodeQuery(GeocodeQuery::create('165.227.0.0'));
+ }
+
+ public function testQuotaExceeded(): void
+ {
+ $this->expectException(\Geocoder\Exception\QuotaExceeded::class);
+
+ $provider = new IPGeolocation($this->getMockedHttpClient('{"message":"You have exceeded the limit"}', 429), 'api_key');
+ $provider->geocodeQuery(GeocodeQuery::create('165.227.0.0'));
+ }
+
+ public function testReverse(): void
+ {
+ $this->expectException(\Geocoder\Exception\UnsupportedOperation::class);
+ $this->expectExceptionMessage('The IPGeolocation provider is not able to do reverse geocoding.');
+
+ $provider = new IPGeolocation($this->getMockedHttpClient(), 'api_key');
+ $provider->reverseQuery(ReverseQuery::fromCoordinates(0, 0));
+ }
+
+ private function getResponse(int $statusCode, string $body): ResponseInterface
+ {
+ return new \Nyholm\Psr7\Response($statusCode, [], $body);
+ }
+
+ /**
+ * The free plan example from the IPGeolocation.io API reference, filtered by the provider's `fields` parameter.
+ */
+ private function getFreePlanBody(): string
+ {
+ return json_encode([
+ 'ip' => '165.227.0.0',
+ 'location' => [
+ 'continent_code' => 'NA',
+ 'continent_name' => 'North America',
+ 'country_code2' => 'US',
+ 'country_code3' => 'USA',
+ 'country_name' => 'United States',
+ 'country_name_official' => 'United States of America',
+ 'country_capital' => 'Washington, D.C.',
+ 'state_prov' => 'California',
+ 'state_code' => 'US-CA',
+ 'district' => 'Santa Clara County',
+ 'city' => 'Santa Clara',
+ 'zipcode' => '95051',
+ 'latitude' => '37.35983',
+ 'longitude' => '-121.98144',
+ 'is_eu' => false,
+ 'country_flag' => 'https://ipgeolocation.io/static/flags/us_64.png',
+ 'geoname_id' => '5346804',
+ 'country_emoji' => 'πΊπΈ',
+ ],
+ 'time_zone' => [
+ 'name' => 'America/Los_Angeles',
+ ],
+ ]);
+ }
+}
diff --git a/src/Provider/IPGeolocation/Tests/IntegrationTest.php b/src/Provider/IPGeolocation/Tests/IntegrationTest.php
new file mode 100644
index 000000000..df90b22da
--- /dev/null
+++ b/src/Provider/IPGeolocation/Tests/IntegrationTest.php
@@ -0,0 +1,39 @@
+getApiKey());
+ }
+
+ protected function getCacheDir(): string
+ {
+ return __DIR__.'/.cached_responses';
+ }
+
+ protected function getApiKey(): string
+ {
+ return $_SERVER['IPGEOLOCATION_API_KEY'];
+ }
+}
diff --git a/src/Provider/IPGeolocation/composer.json b/src/Provider/IPGeolocation/composer.json
new file mode 100644
index 000000000..3912dcb93
--- /dev/null
+++ b/src/Provider/IPGeolocation/composer.json
@@ -0,0 +1,46 @@
+{
+ "name": "geocoder-php/ipgeolocation-provider",
+ "type": "library",
+ "description": "Geocoder IPGeolocation.io adapter",
+ "keywords": [],
+ "homepage": "http://geocoder-php.org/Geocoder/",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "William Durand",
+ "email": "william.durand1@gmail.com"
+ }
+ ],
+ "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\\IPGeolocation\\": ""
+ },
+ "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/IPGeolocation/phpunit.xml.dist b/src/Provider/IPGeolocation/phpunit.xml.dist
new file mode 100644
index 000000000..36cc18c08
--- /dev/null
+++ b/src/Provider/IPGeolocation/phpunit.xml.dist
@@ -0,0 +1,21 @@
+
+
+
+
+ ./
+
+
+ ./Tests
+ ./vendor
+
+
+
+
+
+
+
+
+ ./Tests/
+
+
+