Skip to content

Implement first idea of the cache provider #488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,35 @@ Here is the mapping:

* Timezone: `%T`

### Cache

Sometimes you want to implement a caching mechanism in your application to
reduce your api requests. Geocoder provide two strategy to cache results:

* _Stale If Error_ - Strategy:
This means geocoder tries to ask the underlined provider, if they throws an exception
he tries to become a result from the cache.

* _Expire_ - Strategy:
This is the most used cache mechanism. Geocoder cache each request xxx seconds.

Here an example:

```
// $cacheDriver is a PSR - 6 compatible driver.

$provider = new GoogleMaps();
$cache = new Cache(new ExpireCache($cacheDriver), $provider);

$geocoder->registerProvider($cache);

$geocoder->geocode('Berlin');
$geocoder->geocode('Berlin'); // This uses the cache
```

Geocoder is [PSR - 6](http://www.php-fig.org/psr/psr-6/) compatible and expect
an psr-6 cache driver.


Extending Things
----------------
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"require": {
"php": ">=5.4.0",
"egeloen/http-adapter": "~0.8|~1.0",
"igorw/get-in": "~1.0"
"igorw/get-in": "~1.0",
"psr/cache": "^1.0"
},
"require-dev": {
"geoip2/geoip2": "~2.0",
Expand Down
49 changes: 49 additions & 0 deletions src/Geocoder/CacheStrategy/Expire.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

/**
* This file is part of the Geocoder package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/

namespace Geocoder\CacheStrategy;

use Psr\Cache\CacheItemPoolInterface;

/**
* @author Markus Bachmann <[email protected]>
*/
class Expire implements Strategy
{
private $cache;

private $ttl;

public function __construct(CacheItemPoolInterface $cache, $ttl = null)
{
$this->cache = $cache;
$this->ttl = $ttl;
}

public function invoke($key, callable $function)
{
$item = $this->cache->getItem($key);

if ($item->isHit()) {
return $item->get();
}

$data = call_user_func($function);

if ($this->ttl) {
$item->expiresAfter($this->ttl);
}

$item->set($data);
$this->cache->save($item);

return $data;
}
}
54 changes: 54 additions & 0 deletions src/Geocoder/CacheStrategy/StaleIfError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

/**
* This file is part of the Geocoder package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/

namespace Geocoder\CacheStrategy;

use Psr\Cache\CacheItemPoolInterface;

/**
* @author Markus Bachmann <[email protected]>
*/
class StaleIfError implements Strategy
{
private $cache;

private $ttl;

public function __construct(CacheItemPoolInterface $cache, $ttl = null)
{
$this->cache = $cache;
$this->ttl = $ttl;
}

public function invoke($key, callable $function)
{
$item = $this->cache->getItem($key);

try {
$data = call_user_func($function);
} catch (\Exception $e) {
if (!$item->isHit()) {
throw $e;
}

return $item->get();
}

$item->set($data);

if ($this->ttl) {
$item->expiresAfter($this->ttl);
}

$this->cache->save($item);

return $data;
}
}
19 changes: 19 additions & 0 deletions src/Geocoder/CacheStrategy/Strategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/**
* This file is part of the Geocoder package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/

namespace Geocoder\CacheStrategy;

/**
* @author Markus Bachmann <[email protected]>
*/
interface Strategy
{
function invoke($key, callable $function);
}
101 changes: 101 additions & 0 deletions src/Geocoder/Provider/Cache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

/**
* This file is part of the Geocoder package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/

namespace Geocoder\Provider;

use Geocoder\CacheStrategy\Strategy;

/**
* @author Markus Bachmann <[email protected]>
*/
class Cache implements LocaleAwareProvider
{
use LocaleTrait;

/**
* @var Strategy
*/
private $strategy;

/**
* @var Provider
*/
private $delegate;

/**
* @param CacheItemPoolInterface $cache
* @param Provider $delegate
*/
public function __construct(Strategy $strategy, Provider $delegate)
{
$this->delegate = $delegate;
$this->strategy = $strategy;
}

/**
* {@inheritDoc}
*/
public function geocode($address)
{
$key = $this->generateKey($address);

return $this->strategy->invoke($key, function() use ($address) {
return $this->delegate->geocode($address);
});
}

/**
* {@inheritDoc}
*/
public function reverse($latitude, $longitude)
{
$key = $this->generateKey(serialize([$latitude, $longitude]));

return $this->strategy->invoke($key, function() use ($latitude, $longitude) {
return $this->delegate->reverse($latitude, $longitude);
});
}

/**
* {@inheritDoc}
*/
public function limit($limit)
{
$this->delegate->limit($limit);
}

/**
* {@inheritDoc}
*/
public function getLimit()
{
return $this->delegate->getLimit();
}

/**
* {@inheritDoc}
*/
public function getName()
{
return 'cache';
}

/**
* Generate a key.
*
* @param string $value
*
* @return string
*/
private function generateKey($value)
{
return 'geocoder_'.sha1($value);
}
}
52 changes: 52 additions & 0 deletions tests/Geocoder/Tests/CacheStrategy/ExpireCacheTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Geocoder\Tests\CacheStrategy;

use Geocoder\CacheStrategy\Expire;

class ExpireTest extends \PHPUnit_Framework_TestCase
{
private $strategy;

protected function setUp()
{
$this->pool = $this->prophesize('Psr\Cache\CacheItemPoolInterface');
$this->strategy = new Expire($this->pool->reveal(), 100);
}

public function testInvokeWithCache()
{
$item = $this->prophesize('Psr\Cache\CacheItemInterface');

$item->isHit()->willReturn(true);
$item->get()->willReturn('test');
$item->set()->shouldNotBeCalled();

$this->pool->getItem('foo')->willReturn($item->reveal());

$data = $this->strategy->invoke('foo', function() {});

return $this->assertEquals('test', $data);
}

public function testInvokeWithoutCache()
{
$item = $this->prophesize('Psr\Cache\CacheItemInterface');

$item->isHit()->willReturn(false);
$item->get()->shouldNotBeCalled();
$item->set('test')->shouldBeCalled();
$item->expiresAfter(100)->shouldBeCalled();

$item = $item->reveal();

$this->pool->getItem('foo')->willReturn($item);
$this->pool->save($item)->willReturn(true);

$data = $this->strategy->invoke('foo', function() {
return 'test';
});

return $this->assertEquals('test', $data);
}
}
71 changes: 71 additions & 0 deletions tests/Geocoder/Tests/CacheStrategy/StaleIfErrorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace Geocoder\Tests\CacheStrategy;

use Geocoder\CacheStrategy\StaleIfError;

class StaleIfErrorTest extends \PHPUnit_Framework_TestCase
{
private $strategy;

protected function setUp()
{
$this->pool = $this->prophesize('Psr\Cache\CacheItemPoolInterface');
$this->strategy = new StaleIfError($this->pool->reveal(), 100);
}

public function testValidResponse()
{
$item = $this->prophesize('Psr\Cache\CacheItemInterface');

$item->expiresAfter(100)->shouldBeCalled();
$item->set('test')->shouldBeCalled();

$item->get()->shouldNotBeCalled();

$item = $item->reveal();

$this->pool->getItem('foo')->willReturn($item);
$this->pool->save($item)->willReturn(true);

$data = $this->strategy->invoke('foo', function() {
return 'test';
});

$this->assertEquals('test', $data);
}

public function testCatchExceptionAndReturnCache()
{
$item = $this->prophesize('Psr\Cache\CacheItemInterface');
$item->isHit()->willReturn(true);
$item->get()->willReturn('test');

$item = $item->reveal();

$this->pool->getItem('foo')->willReturn($item);
$this->pool->save()->shouldNotBeCalled();

$data = $this->strategy->invoke('foo', function() {
throw new \Exception();
});

$this->assertEquals('test', $data);
}

/**
* @expectedException \Exception
*/
public function testCatchExceptionAndHaveNoCache()
{
$item = $this->prophesize('Psr\Cache\CacheItemInterface');
$item->isHit()->willReturn(false);
$item->get()->shouldNotBeCalled();

$this->pool->getItem('foo')->willReturn($item->reveal());

$this->strategy->invoke('foo', function() {
throw new \Exception();
});
}
}
Loading