From 0f3815c788dd909a1487038228f0ea312454ad3b Mon Sep 17 00:00:00 2001 From: Baachi Date: Mon, 18 Jan 2016 23:30:05 +0100 Subject: [PATCH 1/4] Implement first idea of the cache provider --- src/Geocoder/Provider/Cache.php | 102 ++++++++++++++++++ .../Provider/CacheStrategy/Expire.php | 37 +++++++ .../Provider/CacheStrategy/StaleIfError.php | 43 ++++++++ .../Provider/CacheStrategy/Strategy.php | 8 ++ 4 files changed, 190 insertions(+) create mode 100644 src/Geocoder/Provider/Cache.php create mode 100644 src/Geocoder/Provider/CacheStrategy/Expire.php create mode 100644 src/Geocoder/Provider/CacheStrategy/StaleIfError.php create mode 100644 src/Geocoder/Provider/CacheStrategy/Strategy.php diff --git a/src/Geocoder/Provider/Cache.php b/src/Geocoder/Provider/Cache.php new file mode 100644 index 000000000..251b7c2cc --- /dev/null +++ b/src/Geocoder/Provider/Cache.php @@ -0,0 +1,102 @@ + + */ +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($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 mixed $value + * + * @return string + */ + private function generateKey($value) + { + return 'geocoder_'.sha1(serialize($value)); + } +} diff --git a/src/Geocoder/Provider/CacheStrategy/Expire.php b/src/Geocoder/Provider/CacheStrategy/Expire.php new file mode 100644 index 000000000..b19c99a5e --- /dev/null +++ b/src/Geocoder/Provider/CacheStrategy/Expire.php @@ -0,0 +1,37 @@ +cache = $cache; + $this->ttl = $ttl; + } + + public function invoke($key, callable $function, CacheItemPoolInterface $cache) + { + $key = $this->generateKey($address); + $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; + } +} diff --git a/src/Geocoder/Provider/CacheStrategy/StaleIfError.php b/src/Geocoder/Provider/CacheStrategy/StaleIfError.php new file mode 100644 index 000000000..3d31f2151 --- /dev/null +++ b/src/Geocoder/Provider/CacheStrategy/StaleIfError.php @@ -0,0 +1,43 @@ +cache = $cache; + $this->ttl = $ttl; + } + + public function invoke($key, callable $function) + { + $item = $this->cache->get($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; + } +} diff --git a/src/Geocoder/Provider/CacheStrategy/Strategy.php b/src/Geocoder/Provider/CacheStrategy/Strategy.php new file mode 100644 index 000000000..6d1737c46 --- /dev/null +++ b/src/Geocoder/Provider/CacheStrategy/Strategy.php @@ -0,0 +1,8 @@ + Date: Mon, 1 Feb 2016 19:57:00 +0100 Subject: [PATCH 2/4] Add tests and move CacheStrategy folder outside --- composer.json | 3 +- .../{Provider => }/CacheStrategy/Expire.php | 9 +-- .../CacheStrategy/StaleIfError.php | 6 +- src/Geocoder/CacheStrategy/Strategy.php | 8 +++ src/Geocoder/Provider/Cache.php | 9 ++- .../Provider/CacheStrategy/Strategy.php | 8 --- .../Tests/CacheStrategy/ExpireCacheTest.php | 52 ++++++++++++++ .../Tests/CacheStrategy/StaleIfErrorTest.php | 71 +++++++++++++++++++ tests/Geocoder/Tests/Provider/CacheTest.php | 57 +++++++++++++++ 9 files changed, 202 insertions(+), 21 deletions(-) rename src/Geocoder/{Provider => }/CacheStrategy/Expire.php (72%) rename src/Geocoder/{Provider => }/CacheStrategy/StaleIfError.php (84%) create mode 100644 src/Geocoder/CacheStrategy/Strategy.php delete mode 100644 src/Geocoder/Provider/CacheStrategy/Strategy.php create mode 100644 tests/Geocoder/Tests/CacheStrategy/ExpireCacheTest.php create mode 100644 tests/Geocoder/Tests/CacheStrategy/StaleIfErrorTest.php create mode 100644 tests/Geocoder/Tests/Provider/CacheTest.php diff --git a/composer.json b/composer.json index 3e456669d..af1c05b2d 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/src/Geocoder/Provider/CacheStrategy/Expire.php b/src/Geocoder/CacheStrategy/Expire.php similarity index 72% rename from src/Geocoder/Provider/CacheStrategy/Expire.php rename to src/Geocoder/CacheStrategy/Expire.php index b19c99a5e..236001989 100644 --- a/src/Geocoder/Provider/CacheStrategy/Expire.php +++ b/src/Geocoder/CacheStrategy/Expire.php @@ -1,8 +1,10 @@ ttl = $ttl; } - public function invoke($key, callable $function, CacheItemPoolInterface $cache) + public function invoke($key, callable $function) { - $key = $this->generateKey($address); $item = $this->cache->getItem($key); if ($item->isHit()) { diff --git a/src/Geocoder/Provider/CacheStrategy/StaleIfError.php b/src/Geocoder/CacheStrategy/StaleIfError.php similarity index 84% rename from src/Geocoder/Provider/CacheStrategy/StaleIfError.php rename to src/Geocoder/CacheStrategy/StaleIfError.php index 3d31f2151..e89021d18 100644 --- a/src/Geocoder/Provider/CacheStrategy/StaleIfError.php +++ b/src/Geocoder/CacheStrategy/StaleIfError.php @@ -1,10 +1,10 @@ cache->get($key); + $item = $this->cache->getItem($key); try { $data = call_user_func($function); diff --git a/src/Geocoder/CacheStrategy/Strategy.php b/src/Geocoder/CacheStrategy/Strategy.php new file mode 100644 index 000000000..d0c669fc0 --- /dev/null +++ b/src/Geocoder/CacheStrategy/Strategy.php @@ -0,0 +1,8 @@ + @@ -57,7 +56,7 @@ public function geocode($address) */ public function reverse($latitude, $longitude) { - $key = $this->generateKey($latitude.$longitude); + $key = $this->generateKey(serialize([$latitude, $longitude])); return $this->strategy->invoke($key, function() use ($latitude, $longitude) { return $this->delegate->reverse($latitude, $longitude); @@ -91,12 +90,12 @@ public function getName() /** * Generate a key. * - * @param mixed $value + * @param string $value * * @return string */ private function generateKey($value) { - return 'geocoder_'.sha1(serialize($value)); + return 'geocoder_'.sha1($value); } } diff --git a/src/Geocoder/Provider/CacheStrategy/Strategy.php b/src/Geocoder/Provider/CacheStrategy/Strategy.php deleted file mode 100644 index 6d1737c46..000000000 --- a/src/Geocoder/Provider/CacheStrategy/Strategy.php +++ /dev/null @@ -1,8 +0,0 @@ -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); + } +} diff --git a/tests/Geocoder/Tests/CacheStrategy/StaleIfErrorTest.php b/tests/Geocoder/Tests/CacheStrategy/StaleIfErrorTest.php new file mode 100644 index 000000000..21b469c1d --- /dev/null +++ b/tests/Geocoder/Tests/CacheStrategy/StaleIfErrorTest.php @@ -0,0 +1,71 @@ +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(); + }); + } +} diff --git a/tests/Geocoder/Tests/Provider/CacheTest.php b/tests/Geocoder/Tests/Provider/CacheTest.php new file mode 100644 index 000000000..8501c7cc3 --- /dev/null +++ b/tests/Geocoder/Tests/Provider/CacheTest.php @@ -0,0 +1,57 @@ +strategy = $this->prophesize('Geocoder\CacheStrategy\Strategy'); + $this->delegate = $this->prophesize('Geocoder\Provider\Provider'); + + $this->provider = new Cache($this->strategy->reveal(), $this->delegate->reveal()); + } + + + public function testGeocode() + { + $this->strategy->invoke( + 'geocoder_3bd614cd786c546800755606b56dfce3863cffa6', + Argument::type('Closure') + )->willReturn('foo'); + + $this->assertEquals('foo', $this->provider->geocode('Alexander Platz 1, Berlin')); + } + + public function testReverse() + { + $this->strategy->invoke( + 'geocoder_20889549b60b05ddbac1f40d34259d8dee5f9835', + Argument::type('Closure') + )->willReturn('foo'); + + $this->assertEquals('foo', $this->provider->reverse(55.56688, 78.51426)); + } + + public function testGetName() + { + $this->assertEquals('cache', $this->provider->getName()); + } + + public function testLimit() + { + $this->delegate->limit(20)->shouldBeCalled(); + + $this->provider->limit(20); + } + + public function testGetLimit() + { + $this->delegate->getLimit()->willReturn(20); + + $this->assertEquals(20, $this->provider->getLimit()); + } +} From 6ec791d4f36d265336fa6cc6960bf3eed6332fca Mon Sep 17 00:00:00 2001 From: Baachi Date: Mon, 1 Feb 2016 20:09:44 +0100 Subject: [PATCH 3/4] Update documentation --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 5fb058598..ef1bcebb6 100644 --- a/README.md +++ b/README.md @@ -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 ---------------- From 51b90d1dabefad55da8212dd08200674428c6f7c Mon Sep 17 00:00:00 2001 From: Baachi Date: Mon, 1 Feb 2016 22:23:19 +0100 Subject: [PATCH 4/4] Add missing headers --- src/Geocoder/CacheStrategy/Expire.php | 11 +++++++++++ src/Geocoder/CacheStrategy/StaleIfError.php | 11 +++++++++++ src/Geocoder/CacheStrategy/Strategy.php | 11 +++++++++++ 3 files changed, 33 insertions(+) diff --git a/src/Geocoder/CacheStrategy/Expire.php b/src/Geocoder/CacheStrategy/Expire.php index 236001989..8ba654b5d 100644 --- a/src/Geocoder/CacheStrategy/Expire.php +++ b/src/Geocoder/CacheStrategy/Expire.php @@ -1,9 +1,20 @@ + */ class Expire implements Strategy { private $cache; diff --git a/src/Geocoder/CacheStrategy/StaleIfError.php b/src/Geocoder/CacheStrategy/StaleIfError.php index e89021d18..3a49fb3e3 100644 --- a/src/Geocoder/CacheStrategy/StaleIfError.php +++ b/src/Geocoder/CacheStrategy/StaleIfError.php @@ -1,9 +1,20 @@ + */ class StaleIfError implements Strategy { private $cache; diff --git a/src/Geocoder/CacheStrategy/Strategy.php b/src/Geocoder/CacheStrategy/Strategy.php index d0c669fc0..2fd5e5b4f 100644 --- a/src/Geocoder/CacheStrategy/Strategy.php +++ b/src/Geocoder/CacheStrategy/Strategy.php @@ -1,7 +1,18 @@ + */ interface Strategy { function invoke($key, callable $function);