├── .github └── workflows │ └── component.yml ├── CHANGELOG.md ├── Exception └── LoopException.php ├── LICENSE ├── Plugin.php ├── Plugin ├── BoundsPlugin.php ├── CachePlugin.php ├── LimitPlugin.php ├── LocalePlugin.php ├── LoggerPlugin.php └── QueryDataPlugin.php ├── PluginProvider.php ├── Promise ├── GeocoderFulfilledPromise.php └── GeocoderRejectedPromise.php ├── Readme.md └── composer.json /.github/workflows/component.yml: -------------------------------------------------------------------------------- 1 | name: Component 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | name: PHP ${{ matrix.php-version }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | php-version: ['8.0', '8.1', '8.2', '8.3', '8.4'] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use PHP ${{ matrix.php-version }} 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php-version }} 23 | extensions: curl 24 | - name: Validate composer.json and composer.lock 25 | run: composer validate --strict 26 | - name: Install dependencies 27 | run: composer update --prefer-stable --prefer-dist --no-progress 28 | - name: Run test suite 29 | run: composer run-script test-ci 30 | - name: Upload Coverage report 31 | run: | 32 | wget https://scrutinizer-ci.com/ocular.phar 33 | php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. 4 | 5 | ## 1.6.0 6 | 7 | ### Added 8 | 9 | - Add support for PHP Geocoder 5 10 | 11 | ## 1.5.0 12 | 13 | ### Added 14 | 15 | - Allow `psr/simple-cache` 2.0 and 3.0 16 | - Add tests and return type for `Promise` 17 | 18 | ### Removed 19 | 20 | - Drop support for PHP 7.3 21 | 22 | ## 1.3.0 23 | 24 | ### Added 25 | 26 | - Add support for PHP 8.0 27 | - Add support for PHP 8.1 28 | - Add GitHub Actions workflow 29 | 30 | ### Removed 31 | 32 | - Drop support for PHP 7.2 33 | 34 | ### Changed 35 | 36 | - Upgrade PHPUnit to version 9 37 | 38 | ## 1.2.0 39 | 40 | ### Removed 41 | 42 | - Drop support for PHP < 7.2 43 | 44 | ## 1.1.0 45 | 46 | ### Added 47 | 48 | - Support for setting permission on the cache plugin. 49 | 50 | ## 1.0.0 51 | 52 | First release of this library. 53 | -------------------------------------------------------------------------------- /Exception/LoopException.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class LoopException extends \RuntimeException implements Exception 24 | { 25 | /** 26 | * @var Query 27 | */ 28 | private $query; 29 | 30 | public static function create(string $message, Query $query): self 31 | { 32 | $ex = new self($message); 33 | $ex->query = $query; 34 | 35 | return $ex; 36 | } 37 | 38 | public function getQuery(): Query 39 | { 40 | return $this->query; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011 — William Durand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Plugin.php: -------------------------------------------------------------------------------- 1 | 27 | * @author Tobias Nyholm 28 | */ 29 | interface Plugin 30 | { 31 | /** 32 | * Handle the Query and return the Collection coming from the next callable. 33 | * 34 | * @param callable $next Next middleware in the chain, the query is passed as the first argument 35 | * @param callable $first First middleware in the chain, used to to restart a request 36 | * 37 | * @return Promise Resolves a Collection or fails with an Geocoder\Exception\Exception 38 | */ 39 | public function handleQuery(Query $query, callable $next, callable $first); 40 | } 41 | -------------------------------------------------------------------------------- /Plugin/BoundsPlugin.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class BoundsPlugin implements Plugin 26 | { 27 | /** 28 | * @var Bounds 29 | */ 30 | private $bounds; 31 | 32 | public function __construct(Bounds $bounds) 33 | { 34 | $this->bounds = $bounds; 35 | } 36 | 37 | public function handleQuery(Query $query, callable $next, callable $first) 38 | { 39 | if (!$query instanceof GeocodeQuery) { 40 | return $next($query); 41 | } 42 | 43 | if (empty($query->getBounds())) { 44 | $query = $query->withBounds($this->bounds); 45 | } 46 | 47 | return $next($query); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Plugin/CachePlugin.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class CachePlugin implements Plugin 27 | { 28 | /** 29 | * @var CacheInterface 30 | */ 31 | private $cache; 32 | 33 | /** 34 | * How log a result is going to be cached. 35 | * 36 | * @var int|null 37 | */ 38 | private $lifetime; 39 | 40 | /** 41 | * @var int|null 42 | */ 43 | private $precision; 44 | 45 | public function __construct(CacheInterface $cache, ?int $lifetime = null, ?int $precision = null) 46 | { 47 | $this->cache = $cache; 48 | $this->lifetime = $lifetime; 49 | $this->precision = $precision; 50 | } 51 | 52 | public function handleQuery(Query $query, callable $next, callable $first) 53 | { 54 | $cacheKey = $this->getCacheKey($query); 55 | if (null !== $cachedResult = $this->cache->get($cacheKey)) { 56 | return $cachedResult; 57 | } 58 | 59 | $result = $next($query); 60 | $this->cache->set($cacheKey, $result, $this->lifetime); 61 | 62 | return $result; 63 | } 64 | 65 | private function getCacheKey(Query $query): string 66 | { 67 | if (null !== $this->precision && $query instanceof ReverseQuery) { 68 | $query = $query->withCoordinates(new Coordinates( 69 | (float) number_format($query->getCoordinates()->getLatitude(), $this->precision), 70 | (float) number_format($query->getCoordinates()->getLongitude(), $this->precision) 71 | )); 72 | } 73 | 74 | // Include the major version number of the geocoder to avoid issues unserializing. 75 | return 'v4'.sha1((string) $query); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Plugin/LimitPlugin.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class LimitPlugin implements Plugin 24 | { 25 | /** 26 | * @var int 27 | */ 28 | private $limit; 29 | 30 | public function __construct(int $limit) 31 | { 32 | $this->limit = $limit; 33 | } 34 | 35 | public function handleQuery(Query $query, callable $next, callable $first) 36 | { 37 | $limit = $query->getLimit(); 38 | if (null !== $limit && $limit > 0) { 39 | $query = $query->withLimit($this->limit); 40 | } 41 | 42 | return $next($query); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Plugin/LocalePlugin.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class LocalePlugin implements Plugin 24 | { 25 | /** 26 | * @var string 27 | */ 28 | private $locale; 29 | 30 | public function __construct(string $locale) 31 | { 32 | $this->locale = $locale; 33 | } 34 | 35 | public function handleQuery(Query $query, callable $next, callable $first) 36 | { 37 | $locale = $query->getLocale(); 38 | if (null === $locale || '' === $locale) { 39 | $query = $query->withLocale($this->locale); 40 | } 41 | 42 | return $next($query); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Plugin/LoggerPlugin.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class LoggerPlugin implements Plugin 27 | { 28 | /** 29 | * @var LoggerInterface 30 | */ 31 | private $logger; 32 | 33 | public function __construct(LoggerInterface $logger) 34 | { 35 | $this->logger = $logger; 36 | } 37 | 38 | public function handleQuery(Query $query, callable $next, callable $first) 39 | { 40 | $startTime = microtime(true); 41 | $logger = $this->logger; 42 | 43 | return $next($query)->then(function (Collection $result) use ($query, $startTime) { 44 | $duration = (microtime(true) - $startTime) * 1000; 45 | $this->logger->info(sprintf('[Geocoder] Got %d results in %0.2f ms for query %s', count($result), $duration, $query->__toString())); 46 | 47 | return $result; 48 | }, function (Exception $exception) use ($query, $startTime) { 49 | $duration = (microtime(true) - $startTime) * 1000; 50 | $this->logger->error(sprintf('[Geocoder] Failed with %s after %0.2f ms for query %s', get_class($exception), $duration, $query->__toString())); 51 | 52 | throw $exception; 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Plugin/QueryDataPlugin.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class QueryDataPlugin implements Plugin 24 | { 25 | /** 26 | * @var array 27 | */ 28 | private $data; 29 | 30 | /** 31 | * @var bool 32 | */ 33 | private $force; 34 | 35 | /** 36 | * @param array $data 37 | * @param bool $force If true we overwrite existing values 38 | */ 39 | public function __construct(array $data, $force = false) 40 | { 41 | $this->data = $data; 42 | $this->force = $force; 43 | } 44 | 45 | public function handleQuery(Query $query, callable $next, callable $first) 46 | { 47 | $queryData = $query->getAllData(); 48 | foreach ($this->data as $key => $value) { 49 | if ($this->force || !array_key_exists($key, $queryData)) { 50 | $query = $query->withData($key, $value); 51 | } 52 | } 53 | 54 | return $next($query); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /PluginProvider.php: -------------------------------------------------------------------------------- 1 | 28 | * @author Tobias Nyholm 29 | */ 30 | class PluginProvider implements Provider 31 | { 32 | /** 33 | * @var Provider 34 | */ 35 | private $provider; 36 | 37 | /** 38 | * @var Plugin[] 39 | */ 40 | private $plugins; 41 | 42 | /** 43 | * A list of options. 44 | * 45 | * @var array{max_restarts?: int<0, max>} 46 | */ 47 | private $options; 48 | 49 | /** 50 | * @param Plugin[] $plugins 51 | * @param array{max_restarts?: int<0, max>} $options 52 | */ 53 | public function __construct(Provider $provider, array $plugins = [], array $options = []) 54 | { 55 | $this->provider = $provider; 56 | $this->plugins = $plugins; 57 | $this->options = $this->configure($options); 58 | } 59 | 60 | public function geocodeQuery(GeocodeQuery $query): Collection 61 | { 62 | $pluginChain = $this->createPluginChain($this->plugins, function (GeocodeQuery $query) { 63 | try { 64 | return new GeocoderFulfilledPromise($this->provider->geocodeQuery($query)); 65 | } catch (Exception $exception) { 66 | return new GeocoderRejectedPromise($exception); 67 | } 68 | }); 69 | 70 | return $pluginChain($query)->wait(); 71 | } 72 | 73 | public function reverseQuery(ReverseQuery $query): Collection 74 | { 75 | $pluginChain = $this->createPluginChain($this->plugins, function (ReverseQuery $query) { 76 | try { 77 | return new GeocoderFulfilledPromise($this->provider->reverseQuery($query)); 78 | } catch (Exception $exception) { 79 | return new GeocoderRejectedPromise($exception); 80 | } 81 | }); 82 | 83 | return $pluginChain($query)->wait(); 84 | } 85 | 86 | public function getName(): string 87 | { 88 | return $this->provider->getName(); 89 | } 90 | 91 | /** 92 | * Configure the plugin provider. 93 | * 94 | * @param array{max_restarts?: int<0, max>} $options 95 | * 96 | * @return array{max_restarts: int<0, max>} 97 | */ 98 | private function configure(array $options = []): array 99 | { 100 | $defaults = [ 101 | 'max_restarts' => 10, 102 | ]; 103 | 104 | $config = array_merge($defaults, $options); 105 | 106 | // Make sure no invalid values are provided 107 | if (count($config) !== count($defaults)) { 108 | throw new LogicException(sprintf('Valid options to the PluginProviders are: %s', implode(', ', array_values($defaults)))); 109 | } 110 | 111 | return $config; 112 | } 113 | 114 | /** 115 | * Create the plugin chain. 116 | * 117 | * @param Plugin[] $pluginList A list of plugins 118 | * @param callable $clientCallable Callable making the HTTP call 119 | * 120 | * @return callable 121 | */ 122 | private function createPluginChain(array $pluginList, callable $clientCallable) 123 | { 124 | $firstCallable = $lastCallable = $clientCallable; 125 | 126 | while ($plugin = array_pop($pluginList)) { 127 | $lastCallable = function (Query $query) use ($plugin, $lastCallable, &$firstCallable) { 128 | return $plugin->handleQuery($query, $lastCallable, $firstCallable); 129 | }; 130 | 131 | $firstCallable = $lastCallable; 132 | } 133 | 134 | $firstCalls = 0; 135 | $firstCallable = function (Query $query) use ($lastCallable, &$firstCalls) { 136 | if ($firstCalls > $this->options['max_restarts']) { 137 | throw LoopException::create('Too many restarts in plugin provider', $query); 138 | } 139 | 140 | ++$firstCalls; 141 | 142 | return $lastCallable($query); 143 | }; 144 | 145 | return $firstCallable; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Promise/GeocoderFulfilledPromise.php: -------------------------------------------------------------------------------- 1 | 19 | * @author Tobias Nyholm 20 | */ 21 | final class GeocoderFulfilledPromise implements Promise 22 | { 23 | /** 24 | * @var Collection 25 | */ 26 | private $collection; 27 | 28 | public function __construct(Collection $collection) 29 | { 30 | $this->collection = $collection; 31 | } 32 | 33 | public function then(?callable $onFulfilled = null, ?callable $onRejected = null): Promise 34 | { 35 | if (null === $onFulfilled) { 36 | return $this; 37 | } 38 | 39 | try { 40 | return new self($onFulfilled($this->collection)); 41 | } catch (Exception $e) { 42 | return new GeocoderRejectedPromise($e); 43 | } 44 | } 45 | 46 | public function getState(): string 47 | { 48 | return Promise::FULFILLED; 49 | } 50 | 51 | public function wait($unwrap = true) 52 | { 53 | if ($unwrap) { 54 | return $this->collection; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Promise/GeocoderRejectedPromise.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Tobias Nyholm 19 | */ 20 | final class GeocoderRejectedPromise implements Promise 21 | { 22 | /** 23 | * @var Exception 24 | */ 25 | private $exception; 26 | 27 | public function __construct(Exception $exception) 28 | { 29 | $this->exception = $exception; 30 | } 31 | 32 | public function then(?callable $onFulfilled = null, ?callable $onRejected = null): Promise 33 | { 34 | if (null === $onRejected) { 35 | return $this; 36 | } 37 | 38 | try { 39 | return new GeocoderFulfilledPromise($onRejected($this->exception)); 40 | } catch (Exception $e) { 41 | return new self($e); 42 | } 43 | } 44 | 45 | public function getState(): string 46 | { 47 | return Promise::REJECTED; 48 | } 49 | 50 | public function wait($unwrap = true) 51 | { 52 | if ($unwrap) { 53 | throw $this->exception; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Geocoder plugin 2 | 3 | [![Build Status](https://travis-ci.org/geocoder-php/plugin.svg?branch=master)](http://travis-ci.org/geocoder-php/plugin) 4 | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/plugin/v/stable)](https://packagist.org/packages/geocoder-php/plugin) 5 | [![Total Downloads](https://poser.pugx.org/geocoder-php/plugin/downloads)](https://packagist.org/packages/geocoder-php/plugin) 6 | [![Monthly Downloads](https://poser.pugx.org/geocoder-php/plugin/d/monthly.png)](https://packagist.org/packages/geocoder-php/plugin) 7 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/geocoder-php/plugin.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/plugin) 8 | [![Quality Score](https://img.shields.io/scrutinizer/g/geocoder-php/plugin.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/plugin) 9 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 10 | 11 | 12 | ### Install 13 | 14 | ```bash 15 | composer require geocoder-php/plugin 16 | ``` 17 | 18 | ### Contribute 19 | 20 | Contributions are very welcome! Send a pull request to the [main repository](https://github.com/geocoder-php/Geocoder) or 21 | report any issues you find on the [issue tracker](https://github.com/geocoder-php/Geocoder/issues). 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geocoder-php/plugin", 3 | "type": "library", 4 | "description": "Plugins to Geocoder providers", 5 | "keywords": [ 6 | "geocoder plugin" 7 | ], 8 | "homepage": "http://geocoder-php.org", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Tobias Nyholm", 13 | "email": "tobias.nyholm@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.0", 18 | "php-http/promise": "^1.0", 19 | "psr/log": "^1.0|^2.0|^3.0", 20 | "psr/simple-cache": "^1.0|^2.0|^3.0", 21 | "willdurand/geocoder": "^4.0|^5.0" 22 | }, 23 | "require-dev": { 24 | "cache/void-adapter": "^1.0", 25 | "phpunit/phpunit": "^9.6.11" 26 | }, 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "1.0-dev" 30 | } 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Geocoder\\Plugin\\": "" 35 | }, 36 | "exclude-from-classmap": [ 37 | "/Tests/" 38 | ] 39 | }, 40 | "minimum-stability": "dev", 41 | "prefer-stable": true, 42 | "scripts": { 43 | "test": "vendor/bin/phpunit", 44 | "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml" 45 | }, 46 | "config": { 47 | "allow-plugins": { 48 | "php-http/discovery": false 49 | } 50 | } 51 | } 52 | --------------------------------------------------------------------------------