Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions config/location.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,30 @@
'connect_timeout' => 3,
],

/*
|--------------------------------------------------------------------------
| Cache
|--------------------------------------------------------------------------
|
| Here you may configure the cache settings for geolocated IP results.
| When enabled, resolved positions are cached to avoid repeated API
| calls for the same IP address.
|
| ttl: duration in seconds to keep results in the cache (default: 1 hour).
| store: the cache store to use, or null to use the default store.
| prefix: the prefix used for cache keys.
| ignore_failed: whether to ignore failed cache attempts (default: true).
|
*/

'cache' => [
'enabled' => env('LOCATION_CACHE', false),
'ttl' => env('LOCATION_CACHE_TTL', 3600),
'store' => env('LOCATION_CACHE_STORE', null),
'prefix' => env('LOCATION_CACHE_PREFIX', 'location'),
'ignore_failed' => env('LOCATION_CACHE_IGNORE_FAILED', true),
],

/*
|--------------------------------------------------------------------------
| Localhost Testing
Expand Down
71 changes: 68 additions & 3 deletions src/LocationManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,76 @@ public function setDefaultDriver(): static
*/
public function get(?string $ip = null): Position|bool
{
if ($location = $this->driver->get($this->request()->setIp($ip))) {
return $location;
$request = $this->request()->setIp($ip);

if (! $this->cacheEnabled()) {
return $this->driver->get($request);
}

$key = $this->cacheKey($request->getIp());

$cache = cache()->store($this->cacheStore());

/**
* The value can be:
*
* - Position instance (cache hit)
* - false (cache hit for failed lookup, if ignore_failed is false)
* - null (cache miss)
*
* @var Position|false|null $position
*/
$position = $cache->get($key);

if (! is_null($position)) {
if ($position instanceof Position) {
$position->cached = true;
}

return $position;
}

$position = $this->driver->get($request);

$ignoreFailed = config('location.cache.ignore_failed', true);

if (! $ignoreFailed || ($position !== false && ! $position->isEmpty())) {
$cache->put($key, $position, $this->cacheTtl());
}

return false;
return $position;
}

/**
* Determine whether the cache is enabled.
*/
protected function cacheEnabled(): bool
{
return (bool) config('location.cache.enabled', false);
}

/**
* Get the cache store to use.
*/
protected function cacheStore(): ?string
{
return config('location.cache.store');
}

/**
* Get the cache TTL.
*/
protected function cacheTtl(): int
{
return (int) config('location.cache.ttl', 3600);
}

/**
* Get the cache key for an IP address.
*/
protected function cacheKey(string $ip): string
{
return config('location.cache.prefix', 'location') . '_' . $ip;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/Position.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ class Position implements Arrayable
*/
public ?string $timezone = null;

/**
* If the location was retrieved from cache.
*/
public bool $cached = false;

/**
* Create a new position instance.
*/
Expand Down
2 changes: 2 additions & 0 deletions tests/Drivers/CloudflareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
'areaCode' => null,
'timezone' => 'Europe/London',
'driver' => Cloudflare::class,
'cached' => false,
]);
});

Expand Down Expand Up @@ -77,6 +78,7 @@
'areaCode' => null,
'timezone' => null,
'driver' => Cloudflare::class,
'cached' => false,
]);
});

Expand Down
1 change: 1 addition & 0 deletions tests/Drivers/GeoPluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@
'ip' => '66.102.0.0',
'timezone' => 'America/Toronto',
'driver' => get_class($driver),
'cached' => false,
]);
});
1 change: 1 addition & 0 deletions tests/Drivers/Ip2locationioTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@
'currencyCode' => null,
'timezone' => '-07:00',
'driver' => get_class($driver),
'cached' => false,
]);
});
1 change: 1 addition & 0 deletions tests/Drivers/IpApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@
'currencyCode' => 'USD',
'timezone' => 'America/Toronto',
'driver' => get_class($driver),
'cached' => false,
]);
});
1 change: 1 addition & 0 deletions tests/Drivers/IpDataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@
'currencyCode' => 'USD',
'timezone' => 'America/Toronto',
'driver' => get_class($driver),
'cached' => false,
]);
});
1 change: 1 addition & 0 deletions tests/Drivers/IpInfoLiteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@
'ip' => '66.102.0.0',
'timezone' => null,
'driver' => get_class($driver),
'cached' => false,
]);
});
1 change: 1 addition & 0 deletions tests/Drivers/IpInfoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@
'ip' => '66.102.0.0',
'timezone' => 'America/Toronto',
'driver' => get_class($driver),
'cached' => false,
]);
});
1 change: 1 addition & 0 deletions tests/Drivers/KloudendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
'currencyCode' => 'USD',
'timezone' => 'America/Toronto',
'driver' => get_class($driver),
'cached' => false,
]);
});

Expand Down
3 changes: 3 additions & 0 deletions tests/Drivers/MaxMindTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
'ip' => '66.102.0.0',
'timezone' => 'America/Toronto',
'driver' => get_class($driver),
'cached' => false,
]);
});

Expand Down Expand Up @@ -97,6 +98,7 @@
'areaCode' => null,
'timezone' => 'Europe/London',
'driver' => "Stevebauman\Location\Drivers\MaxMind",
'cached' => false,
]);
});

Expand Down Expand Up @@ -127,5 +129,6 @@
'areaCode' => null,
'timezone' => null,
'driver' => "Stevebauman\Location\Drivers\MaxMind",
'cached' => false,
]);
});
163 changes: 163 additions & 0 deletions tests/LocationCacheTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

namespace Stevebauman\Location\Tests;

use Illuminate\Support\Facades\Cache;
use Mockery as m;
use Stevebauman\Location\Drivers\Driver;
use Stevebauman\Location\Facades\Location;
use Stevebauman\Location\Position;

beforeEach(function () {
config([
'location.cache.enabled' => true,
'location.cache.ttl' => 3600,
'location.cache.store' => 'array',
'location.cache.prefix' => 'location',
]);

Cache::store(config('location.cache.store'))->flush();
});

/**
* Get the configured cache store.
*/
function cacheStore(): \Illuminate\Cache\Repository
{
return Cache::store(config('location.cache.store'));
}

it('does not use the cache when cache is disabled', function () {
config(['location.cache.enabled' => false]);

$driver = m::mock(Driver::class)
->shouldAllowMockingProtectedMethods();

$driver->shouldReceive('get')
->twice()
->andReturn(new Position);

Location::setDriver($driver);

Location::get('1.2.3.4');
Location::get('1.2.3.4');
});

it('caches a resolved position', function () {
$position = new Position;
$position->ip = '1.2.3.4';
$position->countryCode = 'US';

$driver = m::mock(Driver::class)
->shouldAllowMockingProtectedMethods();

$driver->shouldReceive('get')
->once()
->andReturn($position);

Location::setDriver($driver);

$first = Location::get('1.2.3.4');
$second = Location::get('1.2.3.4');

expect($first)->toBeInstanceOf(Position::class);
expect($second)->toBeInstanceOf(Position::class);
expect(cacheStore()->has('location_1.2.3.4'))->toBeTrue();
});

it('returns the cached position on subsequent calls', function () {
$position = new Position;
$position->ip = '5.6.7.8';

cacheStore()->put('location_5.6.7.8', $position, 3600);

$driver = m::mock(Driver::class)
->makePartial()
->shouldAllowMockingProtectedMethods();

$driver->shouldReceive('process')->never();

Location::setDriver($driver);

$result = Location::get('5.6.7.8');

expect($result)->toBeInstanceOf(Position::class);
expect($result->ip)->toBe('5.6.7.8');
});

it('does not cache a failed lookup by default', function () {
$driver = m::mock(Driver::class)
->shouldAllowMockingProtectedMethods();

$driver->shouldReceive('get')
->twice()
->andReturn(false);

Location::setDriver($driver);

Location::get('9.9.9.9');
Location::get('9.9.9.9');

expect(cacheStore()->has('location_9.9.9.9'))->toBeFalse();
});

it('caches a failed lookup when ignore_failed is false', function () {
config(['location.cache.ignore_failed' => false]);

$driver = m::mock(Driver::class)
->shouldAllowMockingProtectedMethods();

$driver->shouldReceive('get')
->once()
->andReturn(false);

Location::setDriver($driver);

Location::get('9.9.9.9');
Location::get('9.9.9.9');

expect(cacheStore()->has('location_9.9.9.9'))->toBeTrue();
expect(cacheStore()->get('location_9.9.9.9'))->toBeFalse();
});

it('uses a custom cache prefix', function () {
config(['location.cache.prefix' => 'geo']);

$position = new Position;
$position->ip = '1.2.3.4';
$position->countryCode = 'US';

$driver = m::mock(Driver::class)
->shouldAllowMockingProtectedMethods();

$driver->shouldReceive('get')
->once()
->andReturn($position);

Location::setDriver($driver);

Location::get('1.2.3.4');

expect(cacheStore()->has('geo_1.2.3.4'))->toBeTrue();
expect(cacheStore()->has('location_1.2.3.4'))->toBeFalse();
});

it('caches different IPs independently', function () {
$position = new Position;
$position->countryCode = 'US';

$driver = m::mock(Driver::class)
->shouldAllowMockingProtectedMethods();

$driver->shouldReceive('get')
->twice()
->andReturn($position);

Location::setDriver($driver);

Location::get('1.1.1.1');
Location::get('2.2.2.2');

expect(cacheStore()->has('location_1.1.1.1'))->toBeTrue();
expect(cacheStore()->has('location_2.2.2.2'))->toBeTrue();
});