diff --git a/.editorconfig b/.editorconfig
new file mode 100755
index 0000000..f7bb3b6
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*{.yml,.yaml,.json}]
+indent_size = 2
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100755
index 0000000..f388950
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: mckenziearts
diff --git a/.github/workflows/pint.yml b/.github/workflows/pint.yml
new file mode 100755
index 0000000..cccd17f
--- /dev/null
+++ b/.github/workflows/pint.yml
@@ -0,0 +1,25 @@
+name: pint
+
+on: push
+
+jobs:
+ pint:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ github.head_ref }}
+ - name: Set up PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.3
+ - name: Install Composer
+ run: composer install --no-interaction
+ - name: Run Laravel Pint
+ run: composer pint
+ - name: Commit changes
+ uses: stefanzweifel/git-auto-commit-action@v4
+ with:
+ commit_message: Apply formatting
diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000..6d7e08a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/node_modules
+/vendor
+.idea
+.DS_Store
+composer.lock
+.phpunit.result.cache
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..4a69f54
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Arthur Monney
+
+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/README.md b/README.md
new file mode 100644
index 0000000..71a943b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+# [](https://www.abstractapi.com) Abstract’s IP Geolocation API Laravel Client Library
+
+### Getting Started
+
+Abstract’s IP Geolocation API is a fast, lightweight, modern, and RESTful JSON API for determining the location and other details of IP addresses from over 190 countries.
+
+The free plan is limited to 1,000 requests per month. To enable all the data fields and additional request volumes see [https://www.abstractapi.com/api/ip-geolocation-api#pricing](https://www.abstractapi.com/api/ip-geolocation-api#pricing).
+
+### Installation
+
+The package works with PHP 8 and is available using [Composer](https://getcomposer.org).
+
+```shell
+composer require laravelcm/abstract-ip-geolocation
+```
+
+### Usage
+
+Open your application's `\app\Http\Kernel.php` file and add the following to the `Kernel::middleware` property:
+
+```php
+protected $middleware = [
+ ...
+ \Laravelcm\AbstractIpGeolocation\Middleware\AbstractIpGeolocation::class,
+];
+```
+
+#### Quick Start
+
+```php
+Route::get('/', function () {
+ $location = "The IP address " . request()->get('abstract-ip-geolocation')['ip_address'];
+ return view('index', ['location' => $location]);
+});
+```
+
+Will return the following string to the `index` view:
+
+```shell
+"The IP address 127.0.0.1."
+```
+
+### Configuration
+
+wip..
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..5177411
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,56 @@
+{
+ "name": "laravelcm/abstract-ip-geolocation",
+ "description": "Abstract’s IP Geolocation API is a fast, lightweight, modern, and RESTful JSON API for determining the location and other details of IP addresses from over 190 countries.",
+ "license": "MIT",
+ "keywords": [
+ "laravel",
+ "ip-geolocation",
+ "abstract-api"
+ ],
+ "authors": [
+ {
+ "name": "Arthur Monney",
+ "email": "monneylobe@gmail.com"
+ }
+ ],
+ "require": {
+ "php": "^8.2",
+ "ext-json": "*",
+ "illuminate/support": "^10.0|^11.0",
+ "spatie/laravel-package-tools": "^1.15"
+ },
+ "require-dev": {
+ "orchestra/testbench": "^8.5",
+ "laravel/pint": "^1.1",
+ "pestphp/pest": "^2.34",
+ "mockery/mockery": "^1.6"
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravelcm\\AbstractIpGeolocation\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Laravelcm\\AbstractIpGeolocation\\Tests\\": "tests/"
+ }
+ },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravelcm\\AbstractIpGeolocation\\AbstractIpGeolocationServiceProvider"
+ ]
+ }
+ },
+ "scripts": {
+ "test": "vendor/bin/pest",
+ "pint": "vendor/bin/pint"
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "config": {
+ "allow-plugins": {
+ "pestphp/pest-plugin": true
+ }
+ }
+}
diff --git a/config/abstract-ip-geolocation.php b/config/abstract-ip-geolocation.php
new file mode 100644
index 0000000..d005a84
--- /dev/null
+++ b/config/abstract-ip-geolocation.php
@@ -0,0 +1,92 @@
+ env('ABSTRACT_IP_GEOLOCATION_API_KEY'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | User IP Address
+ |--------------------------------------------------------------------------
+ | By default, Abstract IP Geolocation will detect the ip address. But you can
+ | update this configuration to include the client's ip address every time.
+ |
+ | If this value is set to "true", the ip will be detected using the "ip_selector" config.
+ |
+ */
+
+ 'include_ip' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Geolocation Fields
+ |--------------------------------------------------------------------------
+ | You can include a fields value in the query parameters with a comma
+ | separated list of the top-level keys you want to be returned. For example
+ | "fields => 'city,region'" will return only the city and region in the response.
+ |
+ | see: https://docs.abstractapi.com/ip-geolocation#request-parameters
+ */
+
+ 'fields' => null,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Ip Selector Class
+ |--------------------------------------------------------------------------
+ | You can select which class will be used to retrieve the client's ip address
+ | or you can change it and use your own class to return the ip address.
+ |
+ | In case you use a custom IP selector, you may implement the
+ | \Laravelcm\AbstractIpGeolocation\Contracts\AbstractIpInterface interface
+ |
+ | Available class: "IpSelector", "OriginatingIpSelector"
+ |
+ */
+
+ 'ip_selector' => new IpSelector(),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Cache HTTP Request API
+ |--------------------------------------------------------------------------
+ |
+ |
+ */
+
+ 'cache' => [
+ /*
+ | By default, requests are cached to retrieve the user's ip.
+ | If you use the "Free plan" option, caching can help you save
+ | on requests to the Abstract IP Geolocation API.
+ */
+
+ 'enable' => true,
+
+ /*
+ |
+ */
+
+ 'maxsize' => 5,
+
+ /*
+ |
+ |
+ */
+
+ 'ttl' => 86400,
+ ],
+
+];
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..0c12bb9
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ ./tests
+
+
+
+
diff --git a/pint.json b/pint.json
new file mode 100644
index 0000000..01b216c
--- /dev/null
+++ b/pint.json
@@ -0,0 +1,8 @@
+{
+ "presets": "laravel",
+ "rules": {
+ "concat_space": {
+ "spacing": "one"
+ }
+ }
+}
diff --git a/src/AbstractIpGeolocation.php b/src/AbstractIpGeolocation.php
new file mode 100644
index 0000000..5cf0c5c
--- /dev/null
+++ b/src/AbstractIpGeolocation.php
@@ -0,0 +1,63 @@
+cache) {
+ $cacheResponse = $this->cache->get($this->cacheKey($ipAddress ?? '-1'));
+
+ if ($cacheResponse !== null) {
+ return $cacheResponse;
+ }
+ }
+
+ try {
+ $response = Http::withHeaders([
+ 'content-type' => 'application/json',
+ 'accept' => 'application/json',
+ ])
+ ->get('https://ipgeolocation.abstractapi.com/v1', [
+ 'api_key' => $this->token,
+ 'ip_address' => $ipAddress,
+ 'fields' => $this->fields,
+ ]);
+ } catch (\Exception $e) {
+ throw new AbstractApiGeolocationException($e->getMessage());
+ }
+
+ if ($response->status() === 422) {
+ throw new AbstractApiGeolocationException('The request was aborted due to insufficient API credits. (Free plan)');
+ } elseif ($response->status() >= 400) {
+ throw new AbstractApiGeolocationException('Exception: ' . json_encode([
+ 'status' => $response->status(),
+ 'message' => $response->body(),
+ ]));
+ }
+
+ $result = $response->json();
+
+ $this->cache?->set($this->cacheKey($ipAddress ?? '-1'), $result);
+
+ return $result;
+ }
+
+ protected function cacheKey(string $key): string
+ {
+ return sprintf('%s_v%s', $key, '-1');
+ }
+}
diff --git a/src/AbstractIpGeolocationServiceProvider.php b/src/AbstractIpGeolocationServiceProvider.php
new file mode 100644
index 0000000..39e36ba
--- /dev/null
+++ b/src/AbstractIpGeolocationServiceProvider.php
@@ -0,0 +1,17 @@
+name('abstract-ip-geolocation')
+ ->hasConfigFile();
+ }
+}
diff --git a/src/Contracts/AbstractIpInterface.php b/src/Contracts/AbstractIpInterface.php
new file mode 100644
index 0000000..0eed2d8
--- /dev/null
+++ b/src/Contracts/AbstractIpInterface.php
@@ -0,0 +1,15 @@
+maxsize = config('abstract-ip-geolocation.cache.maxsize');
+ $this->ttl = config('abstract-ip-geolocation.cache.ttl');
+ }
+
+ public function has(string $name): bool
+ {
+ return Cache::has($name);
+ }
+
+ public function get(string $name): mixed
+ {
+ return Cache::get($name);
+ }
+
+ public function set(string $name, $value)
+ {
+ if (! $this->has($name)) {
+ $this->elements[] = $name;
+ }
+
+ Cache::put($name, $value, $this->ttl);
+
+ $this->manageSize();
+ }
+
+ /**
+ * If cache maxsize has been reached, remove oldest elements until limit is reached.
+ */
+ private function manageSize(): void
+ {
+ $overflow = count($this->elements) - $this->maxsize;
+
+ if ($overflow > 0) {
+ foreach (array_slice($this->elements, 0, $overflow) as $name) {
+ if ($this->has($name)) {
+ Cache::forget($name);
+ }
+ }
+ $this->elements = array_slice($this->elements, $overflow);
+ }
+ }
+}
diff --git a/src/Exceptions/AbstractApiGeolocationException.php b/src/Exceptions/AbstractApiGeolocationException.php
new file mode 100644
index 0000000..8b403c2
--- /dev/null
+++ b/src/Exceptions/AbstractApiGeolocationException.php
@@ -0,0 +1,9 @@
+ip();
+ }
+}
diff --git a/src/Middleware/AbstractIpGeolocation.php b/src/Middleware/AbstractIpGeolocation.php
new file mode 100644
index 0000000..ffea4d7
--- /dev/null
+++ b/src/Middleware/AbstractIpGeolocation.php
@@ -0,0 +1,57 @@
+getResponse($request);
+
+ $request->merge(['abstract-ip-geolocation' => $response]);
+
+ return $next($request);
+ }
+
+ /**
+ * Get client's ip response
+ *
+ * @throws AbstractApiGeolocationException
+ */
+ public function getResponse($request): array
+ {
+ $ipAddress = null;
+ $cache = null;
+
+ if (config('abstract-ip-geolocation.include_ip')) {
+ $ipAddress = (config('abstract-ip-geolocation.ip_selector', new IpSelector()))->getIp($request);
+ }
+
+ if (config('abstract-ip-geolocation.cache.enable')) {
+ $cache = new DefaultCache();
+ }
+
+ $response = new IpGeolocation(
+ token: config('abstract-ip-geolocation.api_key'),
+ fields: config('abstract-ip-geolocation.fields'),
+ cache: $cache
+ );
+
+ return $response->details($ipAddress);
+ }
+}
diff --git a/src/OriginatingIpSelector.php b/src/OriginatingIpSelector.php
new file mode 100644
index 0000000..7a07b5e
--- /dev/null
+++ b/src/OriginatingIpSelector.php
@@ -0,0 +1,25 @@
+headers->get('x-forwarded-for');
+
+ if (empty($xForwardedFor)) {
+ $ip = $request->ip();
+ } else {
+ $ips = explode(',', $xForwardedFor);
+ // trim as officially the space comes after each comma separator
+ $ip = trim($ips[0]);
+ }
+
+ return $ip;
+ }
+}
diff --git a/tests/Pest.php b/tests/Pest.php
new file mode 100644
index 0000000..c139c2b
--- /dev/null
+++ b/tests/Pest.php
@@ -0,0 +1,7 @@
+in('Unit');
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..ce9e8ed
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,18 @@
+toBeTrue();
+});