diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..68a969ea --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..69a7d65d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +/.github/ export-ignore +/doc/ export-ignore +/tests/ export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.php-cs-fixer.php export-ignore +/CODE_OF_CONDUCT.md export-ignore +/CONTRIBUTING.md export-ignore +/phpstan.neon.dist export-ignore +/phpstan-baseline.neon export-ignore +/phpunit.xml.dist export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..78d9e7cb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,106 @@ +name: CI +on: [ push, pull_request ] + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + env: + php-version: 8.4 + steps: + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ env.php-version }} + tools: flex + + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Install Composer dependencies" + uses: "ramsey/composer-install@v3" + with: + composer-options: "--optimize-autoloader" + + - name: "Run PHPStan" + run: | + vendor/bin/simple-phpunit --version + vendor/bin/phpstan analyse --no-progress + + php-cs-fixer: + name: PHP-CS-Fixer + runs-on: ubuntu-latest + env: + php-version: 8.4 + PHP_CS_FIXER_IGNORE_ENV: 1 + steps: + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ env.php-version }} + tools: flex, cs2pr + + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Install Composer dependencies" + uses: "ramsey/composer-install@v3" + with: + composer-options: "--optimize-autoloader" + + - name: "Run PHP-CS-Fixer" + run: vendor/bin/php-cs-fixer fix -v --dry-run --using-cache=no --format=checkstyle | cs2pr + + phpunit: + name: PHPUnit (PHP ${{ matrix.php }}) (Symfony ${{ matrix.sf_version }}) + runs-on: ubuntu-latest + strategy: + max-parallel: 10 + fail-fast: false + matrix: + php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ] + sf_version: [ '5.4.*', '6.4.*', '7.2.*', '7.3.*' ] + exclude: + - php: '7.4' + sf_version: '6.4.*' + - php: '8.0' + sf_version: '6.4.*' + - php: '7.4' + sf_version: '7.2.*' + - php: '8.0' + sf_version: '7.2.*' + - php: '8.1' + sf_version: '7.2.*' + - php: '7.4' + sf_version: '7.3.*' + - php: '8.0' + sf_version: '7.3.*' + - php: '8.1' + sf_version: '7.3.*' + + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: flex + coverage: none + + - name: "Install Composer dependencies" + env: + SYMFONY_REQUIRE: ${{ matrix.sf_version }} + uses: "ramsey/composer-install@v3" + with: + composer-options: "--optimize-autoloader" + + - name: "Run tests" + env: + SYMFONY_DEPRECATIONS_HELPER: 'ignoreFile=./tests/baseline-ignore' + run: ./vendor/bin/simple-phpunit -v diff --git a/.gitignore b/.gitignore index 6c81dcbd..09c376e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -composer.phar composer.lock +phpunit.xml vendor/ +.php-cs-fixer.cache +.phpunit.result.cache diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 00000000..7236c5b8 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,14 @@ +in(__DIR__) + ->exclude(__DIR__.'/vendor') +; + +return (new PhpCsFixer\Config()) + ->setRules([ + '@Symfony' => true, + 'no_superfluous_phpdoc_tags' => false, + ]) + ->setFinder($finder) +; diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0dc45af3..00000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: php - -php: - - 5.3 - - 5.4 - - 5.5 - -env: - - SYMFONY_VERSION=2.0.7 - - SYMFONY_VERSION=2.1.* - - SYMFONY_VERSION=2.2.* - - SYMFONY_VERSION=2.3.* - - SYMFONY_VERSION=dev-master - -matrix: - allow_failures: - - env: SYMFONY_VERSION=dev-master - - -before_script: - - curl -s http://getcomposer.org/installer | php - - php composer.phar require symfony/framework-bundle:${SYMFONY_VERSION} --no-update - - php composer.phar update - -script: phpunit --coverage-text diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..249b534b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,299 @@ +# Changelog + +The changelog describes what have been "Added", "Changed", "Removed" or "Fixed" between versions. + +## Version 5.19.0 + +### Changed + +- Allow PSR-18 client +- Updated Geocoder Provider dependency requirements to the latest versions +- Deprecate `httplug_client` option in most provider factories in favour of `http_client`. + +### Fixed + +- Load fakeip plugin before cache + +## Version 5.18.0 + +### Added + +- Add OpenRouteService factory + +## Version 5.17.0 + +### Changed + +- Updated PHP requirement to ^7.4 +- Updated minimum required version for some dependencies +- Deprecate `Address::$errorNames` in favour of `Address::ERROR_NAMES`. + +## Version 5.16.2 + +### Fixed + +- Fix deprecations with Symfony 6.1 + +## Version 5.16.1 + +### Fixed + +- Allow Symfony 6 + +## Version 5.16.0 + +### Added + +- Allow Address validation constraint as php8 attribute usage +- Allow fake IP works for all IPs + +## Version 5.15.0 + +### Added + +- Add locationIQ provider +- Add support for PHP 8 attributes + +### Fixed + +- Fix annotation metadata driver with Doctrine proxies +- Fix ORM tests + +### Removed + +- Remove unnecessary version check for registerAliasForArgument + +## Version 5.14.0 + +### Added + +- Add support for PHP 8.0 + +### Changed + +- Upgrade dependencies to up to date versions + +### Removed + +- Remove PHP 7.2 support +- Remove Symfony 3.4 support + +## Version 5.13.0 + +### Added + +- Add support for api-key based authentication in here factory + +## Version 5.12.0 + +### Added + +- Allow configuring local IP when using FakeIpPlugin + +### Changed + +- Skip geocoding if address has not changed + +## Version 5.11.0 + +### Added + +- Allow an HTTP client to be injected via the constructor of providers +- Add MapboxFactory to docs + +### Fixed + +- replace empty() check + +## Version 5.10.0 + +### Added + +- Add MapboxFactory +- Add GoogleMapsPlacesFactory + +### Fixed + +- Fix tests/deprecations and Symfony 5 support + +## Version 5.9.2 + +### Fixed + +- Only trigger provider deprecation notices when they are explicitly configured + +## Version 5.9.1 + +### Fixed + +- Fix extension cannot replace non-defined argument + +## Version 5.9.0 + +### Added + +- Auto tag dumpers with `bazinga_geocoder.dumper` +- Add api_key for the YandexFactory +- Add option to use Faker library for FakeIpPlugin + +## Version 5.8.0 + +### Added + +- Add logger support to ChainFactory + +### Changed + +- Update FreeGeoIpFactory with freegeoip.app + +## Version 5.7.0 + +### Added + +- Add Algolia provider factory + +### Changed + +- Skip empty address values in listener +- Allow cache lifetime as null +- Update dev dependencies +- Make sure we run action on PRs + +## Version 5.6.0 + +### Added + +- Added missing step in Doctrine documentation +- Address annotation can be used over a getter +- Add docs about autowiring +- Integrate phpstan at level 2 +- Adding github actions + +### Changed + +- Deprecate MapzenFactory +- Deprecate GeoIPsFactory +- Rename Changelog.md to CHANGELOG.md + +### Removed + +- Remove useless phpdocs + +## Version 5.5.0 + +### Added + +- Add autowiring bindings by Provider interface + providerName +- Exposes Here provider to the bundle + +### Fixed + +- Add missing tag for AddressValidator constraint + +### Changed + +- Update readme +- Fix method name +- Drop unmaintained Symfony versions support + +## Version 5.4.0 + +### Added + +- Add address validator constraint + +### Fixed + +- SF 4.2 Compliance +- Fix another SF 4.2 deprecation +- Doc fixes +- Custom vendor location symfony 4 + +## Version 5.3.0 + +### Added + +- Support for Ipstack provider +- Support for adding precision argument for the Cache plugin. + +## Version 5.2.0 + +### Added + +- Support for Nominatim 5.0 + +### Fixed + +- Issue when defining plugins. +- Fixed invalid HTML profiler details table. + +## Version 5.1.2 + +### Fixed + +- Make sure commands not using the container. +- Fixed issue with using custom factories. We do not validate custom factories better. +- We are more relaxed in our requirements for HTTPClients. You may now use the option `http_client`. + +## Version 5.1.1 + +### Fixed + +- Adding commands as services +- Fixed twig paths for webprofiler + +## Version 5.1.0 + +### Added + +- `Collector::clear` to be compatible with Symfony 4. +- Added support for IpInfo. + +## Version 5.0.0 + +Version 5 does only support Symfony 3.3+ and PHP7. We dropped some complexity and added plenty of type hints. + +### Added + +- Support for Geocoder 4.0 +- Provider factories +- Support for plugins + +### Changed + +- Namespace changed from `Bazinga\Bundle\GeocoderBundle` to `Bazinga\GeocoderBundle` +- The "fake IP" feature does not change any environment or Symfony variables. +- Configuration for providers has been changed. We now use factories. + +Before: + +```yaml +bazinga_geocoder: + providers: + bing_maps: + api_key: "Foo" + locale: 'sv' +``` +After: + +```yaml +bazinga_geocoder: + providers: + acme: + factory: "Bazinga\GeocoderBundle\ProviderFactory\BingMapsFactory" + locale: 'sv' + options: + api_key: "foo" +``` + +### Removed + +- `DumperManager` +- `LoggableGeocoder`, use `LoggerPlugin` instead. +- Configuration for default provider (`default_provider`) +- `Bazinga\Bundle\GeocoderBundle\Provider\Cache` was removed, use `CachePlugin` instead. +- All services IDs was removed except `bazinga_geocoder.geocoder` and `geocoder`. + +## Version 4.1.0 + +No changelog before this version diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..fe12b3ba --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,29 @@ + +Contributor Code of Conduct +--------------------------- + +As contributors and maintainers of this project, we pledge to respect all people +who contribute through reporting issues, posting feature requests, updating +documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free +experience for everyone, regardless of level of experience, gender, gender +identity and expression, sexual orientation, disability, personal appearance, +body size, race, age, or religion. + +Examples of unacceptable behavior by participants include the use of sexual +language or imagery, derogatory comments or personal attacks, trolling, public +or private harassment, insults, or other unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct. Project maintainers who do not follow the +Code of Conduct may be removed from the project team. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by opening an issue or contacting one or more of the project +maintainers. + +This Code of Conduct is adapted from the [Contributor +Covenant](http:contributor-covenant.org), version 1.0.0, available at +[http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) diff --git a/DataCollector/GeocoderDataCollector.php b/DataCollector/GeocoderDataCollector.php deleted file mode 100644 index a11ebbb6..00000000 --- a/DataCollector/GeocoderDataCollector.php +++ /dev/null @@ -1,89 +0,0 @@ - - */ -class GeocoderDataCollector extends DataCollector -{ - /** - * @var GeocoderLogger - */ - protected $logger; - - /** - * - * @param GeocoderLogger $logger - */ - public function __construct(GeocoderLogger $logger) - { - $this->logger = $logger; - } - - /** - * {@inheritdoc} - */ - public function collect(Request $request, Response $response, \Exception $exception = null) - { - $this->data = array( - 'requests' => null !== $this->logger ? $this->logger->getRequests() : array(), - ); - } - - /** - * Returns an array of collected requests. - * - * @return array - */ - public function getRequests() - { - return $this->data['requests']; - } - - /** - * Returns the number of collected requests. - * - * @return integer - */ - public function getRequestsCount() - { - return count($this->data['requests']); - } - - /** - * Returns the execution time of all collected requests in seconds. - * - * @return float - */ - public function getTime() - { - $time = 0; - foreach ($this->data['requests'] as $command) { - $time += $command['duration']; - } - - return $time; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'geocoder'; - } -} diff --git a/DependencyInjection/BazingaGeocoderExtension.php b/DependencyInjection/BazingaGeocoderExtension.php deleted file mode 100644 index 1fd4ba89..00000000 --- a/DependencyInjection/BazingaGeocoderExtension.php +++ /dev/null @@ -1,245 +0,0 @@ - - */ -class BazingaGeocoderExtension extends Extension -{ - protected $container; - - public function load(array $configs, ContainerBuilder $container) - { - $this->container = $container; - - $processor = new Processor(); - $configuration = new Configuration(); - $config = $processor->processConfiguration($configuration, $configs); - - $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); - $loader->load('services.xml'); - - if (!empty($config['fake_ip']) && true === $config['fake_ip']['enabled']) { - $definition = $container->getDefinition('bazinga_geocoder.event_listener.fake_request'); - $definition->replaceArgument(0, $config['fake_ip']['ip']); - - $tag = current($definition->getTag('kernel.event_listener')); - $tag['priority'] = $config['fake_ip']['priority']; - $tags = array('kernel.event_listener' => array($tag)); - $definition->setTags($tags); - } else { - $container->removeDefinition('bazinga_geocoder.event_listener.fake_request'); - } - - if (isset($config['adapter']['class']) && !empty($config['adapter']['class'])) { - $container->setParameter('bazinga_geocoder.geocoder.adapter.class', $config['adapter']['class']); - } - - if (isset($config['providers']['free_geo_ip'])) { - $this->addProvider('free_geo_ip'); - } - - if (isset($config['providers']['host_ip'])) { - $this->addProvider('host_ip'); - } - - if (isset($config['providers']['bing_maps'])) { - $bingMapsParams = $config['providers']['bing_maps']; - - $this->addProvider('bing_maps', array( - $bingMapsParams['api_key'], - $bingMapsParams['locale'], - )); - } - - if (isset($config['providers']['ip_info_db'])) { - $ipInfoDbParams = $config['providers']['ip_info_db']; - - $this->addProvider('ip_info_db', array($ipInfoDbParams['api_key'])); - } - - if (isset($config['providers']['yahoo'])) { - $yahooParams = $config['providers']['yahoo']; - - $this->addProvider('yahoo', array( - $yahooParams['api_key'], - $yahooParams['locale'], - )); - } - - if (isset($config['providers']['cloudmade'])) { - $cloudMadeParams = $config['providers']['cloudmade']; - - $this->addProvider('cloudmade', array($cloudMadeParams['api_key'])); - } - - if (isset($config['providers']['google_maps'])) { - $googleMapsParams = $config['providers']['google_maps']; - - $this->addProvider('google_maps', array( - $googleMapsParams['locale'], - $googleMapsParams['region'], - $googleMapsParams['use_ssl'], - )); - } - - if (isset($config['providers']['google_maps_business'])) { - $googleMapsBusinessParams = $config['providers']['google_maps_business']; - - $this->addProvider('google_maps_business', array( - $googleMapsBusinessParams['client_id'], - $googleMapsBusinessParams['api_key'], - $googleMapsBusinessParams['locale'], - $googleMapsBusinessParams['region'], - $googleMapsBusinessParams['use_ssl'], - )); - } - - if (isset($config['providers']['openstreetmaps'])) { - $openstreetMapsParams = $config['providers']['openstreetmaps']; - - $this->addProvider('openstreetmaps', array($openstreetMapsParams['locale'])); - } - - if (isset($config['providers']['geoip'])) { - $this->addProvider('geoip'); - } - - if (isset($config['providers']['mapquest'])) { - $this->addProvider('mapquest'); - } - - if (isset($config['providers']['oiorest'])) { - $this->addProvider('oiorest'); - } - - if (isset($config['providers']['geocoder_ca'])) { - $this->addProvider('geocoder_ca'); - } - - if (isset($config['providers']['geocoder_us'])) { - $this->addProvider('geocoder_us'); - } - - if (isset($config['providers']['ign_openls'])) { - $ignOpenlsParams = $config['providers']['ign_openls']; - - $this->addProvider('ign_openls', array($ignOpenlsParams['api_key'])); - } - - if (isset($config['providers']['data_science_toolkit'])) { - $this->addProvider('data_science_toolkit'); - } - - if (isset($config['providers']['yandex'])) { - $yandexParams = $config['providers']['yandex']; - - $this->addProvider('yandex', array($yandexParams['locale'], $yandexParams['toponym'])); - } - - if (isset($config['providers']['geo_ips'])) { - $this->addProvider('geo_ips', array($config['providers']['geo_ips']['api_key'])); - } - - if (isset($config['providers']['geo_plugin'])) { - $this->addProvider('geo_plugin'); - } - - if (isset($config['providers']['maxmind'])) { - $maxmindParams = $config['providers']['maxmind']; - - $this->addProvider('maxmind', array($maxmindParams['api_key'])); - } - - if (isset($config['providers']['maxmind_binary'])) { - $provider = new Definition( - '%bazinga_geocoder.geocoder.provider.maxmind_binary.class%', - array( - $config['providers']['maxmind_binary']['binary_file'], - $config['providers']['maxmind_binary']['open_flag'], - ) - ); - - $provider - ->setPublic(false) - ->addTag('bazinga_geocoder.provider'); - - $this->container->setDefinition('bazinga_geocoder.provider.maxmind_binary', $provider); - } - - if (isset($config['providers']['cache'])) { - $params = $config['providers']['cache']; - $cache = new Reference($params['adapter']); - $fallback = new Reference('bazinga_geocoder.provider.'.$params['provider']); - - $provider = new Definition( - '%bazinga_geocoder.geocoder.provider.cache.class%', - array($cache, $fallback, $params['lifetime']) - ); - - if (isset($params['locale'])) { - $provider->addArgument($params['locale']); - } - - $provider - ->setPublic(false) - ->addTag('bazinga_geocoder.provider'); - - $container->setDefinition('bazinga_geocoder.provider.cache', $provider); - } - - if (isset($config['providers']['chain'])) { - $chainProvider = new Definition( - '%bazinga_geocoder.geocoder.provider.chain.class%' - ); - - $this->container->setDefinition('bazinga_geocoder.provider.chain', $chainProvider); - - $chainProvider - ->setPublic(false) - ->addTag('bazinga_geocoder.provider'); - - if (isset($config['providers']['chain']['providers'])) { - foreach ($config['providers']['chain']['providers'] as $name) { - if ($this->container->hasDefinition('bazinga_geocoder.provider.'.$name)) { - $chainProvider->addMethodCall('addProvider', array($this->container->getDefinition('bazinga_geocoder.provider.'.$name))); - } - } - } - } - } - - protected function addProvider($name, array $arguments = array()) - { - $provider = new Definition( - '%bazinga_geocoder.geocoder.provider.'.$name.'.class%', - array_merge( - array(new Reference('bazinga_geocoder.geocoder.adapter')), - $arguments - ) - ); - - $provider - ->setPublic(false) - ->addTag('bazinga_geocoder.provider'); - - $this->container->setDefinition('bazinga_geocoder.provider.'.$name, $provider); - } -} diff --git a/DependencyInjection/Compiler/AddDumperPass.php b/DependencyInjection/Compiler/AddDumperPass.php deleted file mode 100644 index 0f7afcb1..00000000 --- a/DependencyInjection/Compiler/AddDumperPass.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -class AddDumperPass implements CompilerPassInterface -{ - /** - * {@inheritDoc} - */ - public function process(ContainerBuilder $container) - { - if (!$container->hasDefinition('bazinga_geocoder.dumper_manager')) { - return; - } - - $manager = $container->findDefinition('bazinga_geocoder.dumper_manager'); - - $dumpers = array(); - foreach ($container->findTaggedServiceIds('geocoder.dumper') as $id => $attributes) { - if (!isset($attributes[0]['alias'])) { - throw new \RuntimeException(sprintf('No alias for service "%s" provided. Please set a alias!', $id)); - } - - $dumpers[$attributes[0]['alias']] = $container->getDefinition($id); - } - - $manager->setArguments(array($dumpers)); - } -} diff --git a/DependencyInjection/Compiler/AddProvidersPass.php b/DependencyInjection/Compiler/AddProvidersPass.php deleted file mode 100644 index 3a522617..00000000 --- a/DependencyInjection/Compiler/AddProvidersPass.php +++ /dev/null @@ -1,50 +0,0 @@ - - */ -class AddProvidersPass implements CompilerPassInterface -{ - /** - * @var \Symfony\Component\DependencyInjection\ContainerBuilder - */ - protected $container; - - /** - * Get all providers based on their tag ('geocoder.provider') and register them. - * - * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container The container. - */ - public function process(ContainerBuilder $container) - { - if (!$container->hasDefinition('bazinga_geocoder.geocoder')) { - return; - } - - $this->container = $container; - - $array = array(); - foreach ($this->container->findTaggedServiceIds('bazinga_geocoder.provider') as $providerId => $attributes) { - $array[] = new Reference($providerId); - } - - $this->container - ->getDefinition('bazinga_geocoder.geocoder') - ->addMethodCall('registerProviders', array($array)) - ; - } -} diff --git a/DependencyInjection/Compiler/LoggablePass.php b/DependencyInjection/Compiler/LoggablePass.php deleted file mode 100644 index 00e9efd4..00000000 --- a/DependencyInjection/Compiler/LoggablePass.php +++ /dev/null @@ -1,38 +0,0 @@ - - */ -class LoggablePass implements CompilerPassInterface -{ - /** - * {@inheritDoc} - */ - public function process(ContainerBuilder $container) - { - if (!$container->hasDefinition('bazinga_geocoder.geocoder')) { - return; - } - - $definition = $container->getDefinition('bazinga_geocoder.geocoder'); - $definition->setClass( - $container->getParameter('bazinga_geocoder.geocoder.loggable_class') - ); - $definition->addMethodCall('setLogger', array(new Reference('bazinga_geocoder.logger'))); - - } -} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php deleted file mode 100644 index e1b22969..00000000 --- a/DependencyInjection/Configuration.php +++ /dev/null @@ -1,194 +0,0 @@ - - */ -class Configuration implements ConfigurationInterface -{ - /** - * Generates the configuration tree builder. - * - * @return TreeBuilder The tree builder - */ - public function getConfigTreeBuilder() - { - $treeBuilder = new TreeBuilder(); - $rootNode = $treeBuilder->root('bazinga_geocoder'); - - $rootNode - ->children() - ->arrayNode('fake_ip') - ->beforeNormalization() - ->ifString() - ->then(function ($value) { return array('ip' => $value); }) - ->end() - ->treatFalseLike(array('enabled' => false)) - ->treatTrueLike(array('enabled' => true)) - ->treatNullLike(array('enabled' => true)) - ->children() - ->booleanNode('enabled') - ->defaultTrue() - ->end() - ->scalarNode('ip')->defaultNull()->end() - ->scalarNode('priority')->defaultValue(0)->end() - ->end() - ->end() - ->arrayNode('adapter') - ->children() - ->scalarNode('class')->defaultNull()->end() - ->end() - ->end() - ->arrayNode('providers') - ->children() - ->arrayNode('bing_maps') - ->children() - ->scalarNode('api_key') - ->isRequired()->cannotBeEmpty() - ->end() - ->scalarNode('locale')->defaultNull()->end() - ->end() - ->end() - ->arrayNode('cache') - ->children() - ->scalarNode('adapter') - ->isRequired() - ->cannotBeEmpty() - ->end() - ->scalarNode('provider') - ->isRequired() - ->cannotBeEmpty() - ->end() - ->scalarNode('locale') - ->defaultNull() - ->end() - ->scalarNode('lifetime') - ->defaultValue(86400) - ->validate() - ->ifTrue(function ($v) { return !is_integer($v); }) - ->thenInvalid('Only integer are allowed!') - ->end() - ->end() - ->end() - ->end() - ->arrayNode('ip_info_db') - ->children() - ->scalarNode('api_key') - ->isRequired()->cannotBeEmpty() - ->end() - ->end() - ->end() - ->arrayNode('yahoo') - ->children() - ->scalarNode('api_key') - ->isRequired()->cannotBeEmpty() - ->end() - ->scalarNode('locale')->defaultNull()->end() - ->end() - ->end() - ->arrayNode('cloudmade') - ->children() - ->scalarNode('api_key') - ->isRequired()->cannotBeEmpty() - ->end() - ->end() - ->end() - ->arrayNode('google_maps') - ->children() - ->scalarNode('locale')->defaultNull()->end() - ->scalarNode('region')->defaultNull()->end() - ->booleanNode('use_ssl')->defaultFalse()->end() - ->end() - ->end() - ->arrayNode('google_maps_business') - ->children() - ->scalarNode('client_id')->isRequired()->cannotBeEmpty()->end() - ->scalarNode('api_key')->defaultNull()->end() - ->scalarNode('locale')->defaultNull()->end() - ->scalarNode('region')->defaultNull()->end() - ->booleanNode('use_ssl')->defaultFalse()->end() - ->end() - ->end() - ->arrayNode('openstreetmaps') - ->children() - ->scalarNode('locale')->defaultNull()->end() - ->end() - ->end() - ->arrayNode('host_ip')->end() - ->arrayNode('geoip')->end() - ->arrayNode('free_geo_ip')->end() - ->arrayNode('mapquest')->end() - ->arrayNode('oiorest')->end() - ->arrayNode('geocoder_ca')->end() - ->arrayNode('geocoder_us')->end() - ->arrayNode('ign_openls') - ->children() - ->scalarNode('api_key') - ->isRequired()->cannotBeEmpty() - ->end() - ->end() - ->end() - ->arrayNode('data_science_toolkit')->end() - ->arrayNode('yandex') - ->children() - ->scalarNode('locale')->defaultNull()->end() - ->scalarNode('toponym')->defaultNull()->end() - ->end() - ->end() - ->arrayNode('geo_ips') - ->children() - ->scalarNode('api_key')->defaultNull()->end() - ->end() - ->end() - ->arrayNode('geo_plugin')->end() - ->arrayNode('maxmind') - ->children() - ->scalarNode('api_key') - ->isRequired()->cannotBeEmpty() - ->end() - ->end() - ->end() - ->arrayNode('maxmind_binary') - ->children() - ->scalarNode('binary_file') - ->isRequired()->cannotBeEmpty() - ->end() - ->scalarNode('open_flag') - ->defaultValue(null) - ->end() - ->end() - ->end() - ->arrayNode('chain') - ->fixXmlConfig('provider') - ->children() - ->arrayNode('providers') - ->prototype('scalar')->end() - ->end() - ->end() - ->end() - ->arrayNode('tom_tom') - ->children() - ->scalarNode('api_key') - ->isRequired()->cannotBeEmpty() - ->end() - ->scalarNode('locale')->defaultNull()->end() - ->end() - ->end() - ->end() - ; - - return $treeBuilder; - } -} diff --git a/DumperManager.php b/DumperManager.php deleted file mode 100644 index e3ae390b..00000000 --- a/DumperManager.php +++ /dev/null @@ -1,94 +0,0 @@ - - */ -class DumperManager -{ - /** - * @var array - */ - private $dumpers; - - /** - * Constructor - * - * @param array $dumpers - */ - public function __construct(array $dumpers = array()) - { - $this->dumpers = array(); - - foreach ($dumpers as $name => $dumper) { - $this->set($name, $dumper); - } - } - - /** - * Get a dumper - * - * @param string $name The name of the dumper - * - * @return DumperInterface - * - * @throws \RuntimeException If no dumper was found - */ - public function get($name) - { - if (!isset($this->dumpers[$name])) { - throw new \RuntimeException(sprintf('The dumper "%s" does not exist', $name)); - } - - return $this->dumpers[$name]; - } - - /** - * Sets a dumper - * - * @param string $name The name - * @param DumperInterface $dumper The dumper instance - */ - public function set($name, DumperInterface $dumper) - { - $this->dumpers[$name] = $dumper; - } - - /** - * Remove a dumper instance from the manager - * - * @param string $name The name of the dumper - * - * @throws \RuntimeException If no dumper was found - */ - public function remove($name) - { - if (!isset($this->dumpers[$name])) { - throw new \RuntimeException(sprintf('The dumper "%s" does not exist', $name)); - } - - unset($this->dumpers[$name]); - } - - /** - * Return true if $name exists, false otherwise. - * - * @param $name The name of the dumper - * - * @return bool - */ - public function has($name) - { - return array_key_exists($name, $this->dumpers); - } -} diff --git a/EventListener/FakeRequestListener.php b/EventListener/FakeRequestListener.php deleted file mode 100644 index 8ca13bc3..00000000 --- a/EventListener/FakeRequestListener.php +++ /dev/null @@ -1,46 +0,0 @@ - - */ -class FakeRequestListener -{ - /** - * @var string - */ - protected $fakeIp = null; - - public function __construct($fakeIp) - { - $this->fakeIp = $fakeIp; - } - - public function onKernelRequest(GetResponseEvent $event) - { - if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) { - return; - } - - if (null !== $this->fakeIp && !empty($this->fakeIp)) { - $event->getRequest()->server->set('REMOTE_ADDR', $this->fakeIp); - } - } - - public function getFakeIp() - { - return $this->fakeIp; - } -} diff --git a/Geocoder/LoggableGeocoder.php b/Geocoder/LoggableGeocoder.php deleted file mode 100644 index 8bf3d84e..00000000 --- a/Geocoder/LoggableGeocoder.php +++ /dev/null @@ -1,86 +0,0 @@ -logger = $logger; - } - - /** - * {@inheritdoc} - */ - public function geocode($value) - { - if (null === $this->logger) { - return parent::geocode($value); - } - - $startTime = microtime(true); - $result = parent::geocode($value); - $duration = (microtime(true) - $startTime) * 1000; - - $this->logger->logRequest( - sprintf("[Geocoding] %s", $value), - $duration, - $this->getProviderClass(), - json_encode($result->toArray()) - ); - - return $result; - } - - /** - * {@inheritDoc} - */ - public function reverse($latitude, $longitude) - { - if (null === $this->logger) { - return parent::reverse($latitude, $longitude); - } - - $startTime = microtime(true); - $result = parent::reverse($latitude, $longitude); - $duration = (microtime(true) - $startTime) * 1000; - - $value = sprintf("[Reverse geocoding] latitude: %s, longitude: %s", $latitude, $longitude); - - $this->logger->logRequest( - $value, - $duration, - $this->getProviderClass(), - json_encode($result->toArray()) - ); - - return $result; - } - - protected function getProviderClass() - { - $provider = explode('\\', get_class($this->getProvider())); - - return end($provider); - } -} diff --git a/Resources/meta/LICENSE b/LICENSE similarity index 93% rename from Resources/meta/LICENSE rename to LICENSE index 71b39b91..a6b7b809 100644 --- a/Resources/meta/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2013 William Durand +Copyright (c) Since 2011 — William Durand Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Logger/GeocoderLogger.php b/Logger/GeocoderLogger.php deleted file mode 100644 index 44f46955..00000000 --- a/Logger/GeocoderLogger.php +++ /dev/null @@ -1,67 +0,0 @@ - - */ -class GeocoderLogger -{ - protected $logger; - protected $nbRequests = 0; - protected $requests = array(); - - /** - * - * @param LoggerInterface $logger - */ - public function __construct(LoggerInterface $logger = null) - { - $this->logger = $logger; - } - - /** - * - * @param string $value value to geocode - * @param float $duration - * @param string $providerClass Geocoder provider class - * @param mixed $result - */ - public function logRequest($value, $duration, $providerClass, $result) - { - ++$this->nbRequests; - - if (null !== $this->logger) { - $this->requests[] = array( - 'value' => $value, - 'duration' => $duration, - 'providerClass' => $providerClass, - 'result' => $result - ); - - $message = sprintf("%s %0.2f ms (%s)", $value, $duration, $providerClass); - $this->logger->info($message); - } - } - - /** - * Returns an array of the logged requests. - * - * @return array - */ - public function getRequests() - { - return $this->requests; - } -} diff --git a/Provider/CacheProvider.php b/Provider/CacheProvider.php deleted file mode 100644 index 710dbeed..00000000 --- a/Provider/CacheProvider.php +++ /dev/null @@ -1,98 +0,0 @@ - - */ -class CacheProvider implements ProviderInterface -{ - /** - * @var \Doctrine\Common\Cache\Cache - */ - private $cache; - - /** - * @var \Geocoder\Provider\ProviderInterface - */ - private $provider; - - /** - * @var integer - */ - private $lifetime; - - /** - * @var string|null - */ - private $locale; - - /** - * Constructor - * - * @param Cache $cache The cache interface - * @param ProviderInterface $provider The fallback provider - * @param integer $lifetime The cache lifetime - * @param string $locale - */ - public function __construct(Cache $cache, ProviderInterface $provider, $lifetime = 0, $locale = null) - { - $this->cache = $cache; - $this->provider = $provider; - $this->lifetime = $lifetime; - $this->locale = $locale; - } - - /** - * {@inheritDoc} - */ - public function getGeocodedData($address) - { - $key = crc32($this->locale.$address); - - if (false !== $data = $this->cache->fetch($key)) { - return unserialize($data); - } - - $data = $this->provider->getGeocodedData($address); - $this->cache->save($key, serialize($data), $this->lifetime); - - return $data; - } - - /** - * {@inheritDoc} - */ - public function getReversedData(array $coordinates) - { - $key = crc32(serialize($this->locale . json_encode($coordinates))); - - if (false !== $data = $this->cache->fetch($key)) { - return unserialize($data); - } - - $data = $this->provider->getReversedData($coordinates); - $this->cache->save($key, serialize($data), $this->lifetime); - - return $data; - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return 'cache'; - } -} diff --git a/README.md b/README.md index a4e3c1f2..ed2404d2 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,24 @@ BazingaGeocoderBundle ===================== -[![Build -Status](https://secure.travis-ci.org/geocoder-php/BazingaGeocoderBundle.png)](http://travis-ci.org/geocoder-php/BazingaGeocoderBundle) +[![Build Status](https://github.com/geocoder-php/BazingaGeocoderBundle/actions/workflows/ci.yml/badge.svg)](https://github.com/geocoder-php/BazingaGeocoderBundle/actions/workflows/ci.yml) +[![Latest Stable Version](https://poser.pugx.org/willdurand/geocoder-bundle/v/stable)](https://packagist.org/packages/willdurand/geocoder-bundle) +[![Total Downloads](https://poser.pugx.org/willdurand/geocoder-bundle/downloads)](https://packagist.org/packages/willdurand/geocoder-bundle) +[![Monthly Downloads](https://poser.pugx.org/willdurand/geocoder-bundle/d/monthly.png)](https://packagist.org/packages/willdurand/geocoder-bundle) +[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/geocoder-php/BazingaGeocoderBundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/BazingaGeocoderBundle) +[![Quality Score](https://img.shields.io/scrutinizer/g/geocoder-php/BazingaGeocoderBundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/BazingaGeocoderBundle) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) Integration of the [**Geocoder**](http://github.com/geocoder-php/Geocoder) library -into Symfony2. +into Symfony. It integrates seamlessly with the Symfony profiler. You can +check number of queries executed by each provider, total execution time +and geocoding results. + +![Example Toolbar](doc/toolbar.png) + + +![Example Profiler page](doc/profiler-page.png) + Documentation @@ -13,16 +26,16 @@ Documentation For documentation, see: - Resources/doc/ + doc/ -[Read the documentation](https://github.com/geocoder-php/BazingaGeocoderBundle/blob/master/Resources/doc/index.md) +[Read the documentation](doc/index.md) Contributing ------------ See -[CONTRIBUTING](https://github.com/geocoder-php/BazingaGeocoderBundle/blob/master/CONTRIBUTING.md) +[CONTRIBUTING](CONTRIBUTING.md) file. @@ -30,13 +43,18 @@ Credits ------- * William Durand +* Tobias Nyholm * [All contributors](https://github.com/geocoder-php/BazingaGeocoderBundle/contributors) +Contributor Code of Conduct +--------------------------- + +Please note that this project is released with a Contributor Code of Conduct. +By participating in this project you agree to abide by its terms. + + License ------- -This bundle is released under the MIT license. See the complete license in the -bundle: - - Resources/meta/LICENSE +This bundle is released under the MIT license. See the complete license [here](LICENSE). diff --git a/Resources/config/services.xml b/Resources/config/services.xml deleted file mode 100644 index fbc13869..00000000 --- a/Resources/config/services.xml +++ /dev/null @@ -1,95 +0,0 @@ - - - - - Geocoder\Geocoder - Bazinga\Bundle\GeocoderBundle\Geocoder\LoggableGeocoder - Geocoder\HttpAdapter\CurlHttpAdapter - Geocoder\Provider\BingMapsProvider - Geocoder\Provider\FreeGeoIpProvider - Geocoder\Provider\GoogleMapsProvider - Geocoder\Provider\GoogleMapsBusinessProvider - Geocoder\Provider\HostIpProvider - Geocoder\Provider\IpInfoDbProvider - Geocoder\Provider\YahooProvider - Geocoder\Provider\CloudMadeProvider - Geocoder\Provider\OpenStreetMapsProvider - Geocoder\Provider\GeoipProvider - Geocoder\Provider\MapQuestProvider - Geocoder\Provider\OIORestProvider - Geocoder\Provider\GeocoderCaProvider - Geocoder\Provider\GeocoderUsProvider - Geocoder\Provider\IGNOpenLSProvider - Geocoder\Provider\DataScienceToolkitProvider - Geocoder\Provider\YandexProvider - Geocoder\Provider\GeoIPsProvider - Geocoder\Provider\GeoPluginProvider - Geocoder\Provider\MaxMindProvider - Geocoder\Provider\MaxMindBinaryProvider - Bazinga\Bundle\GeocoderBundle\Provider\CacheProvider - Geocoder\Provider\ChainProvider - Geocoder\Provider\TomTomProvider - - - Bazinga\Bundle\GeocoderBundle\Logger\GeocoderLogger - - Bazinga\Bundle\GeocoderBundle\DataCollector\GeocoderDataCollector - - Bazinga\Bundle\GeocoderBundle\EventListener\FakeRequestListener - - Bazinga\Bundle\GeocoderBundle\DumperManager - Geocoder\Dumper\GeoJsonDumper - Geocoder\Dumper\GpxDumper - Geocoder\Dumper\KmlDumper - Geocoder\Dumper\WkbDumper - Geocoder\Dumper\WktDumper - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Resources/doc/index.md b/Resources/doc/index.md deleted file mode 100644 index 432b91bf..00000000 --- a/Resources/doc/index.md +++ /dev/null @@ -1,280 +0,0 @@ -BazingaGeocoderBundle -===================== - -Integration of the [**Geocoder**](http://github.com/willdurand/Geocoder) library -into Symfony2. - - -Installation ------------- - -Require [`willdurand/geocoder-bundle`](https://packagist.org/packages/willdurand/geocoder-bundle) -to your `composer.json` file: - - -```json -{ - "require": { - "willdurand/geocoder-bundle": "@stable" - } -} -``` - -Register the bundle in `app/AppKernel.php`: - - // app/AppKernel.php - public function registerBundles() - { - return array( - // ... - new Bazinga\Bundle\GeocoderBundle\BazingaGeocoderBundle(), - ); - } - -Enable the bundle's configuration in `app/config/config.yml`: - -``` yaml -# app/config/config.yml -bazinga_geocoder: ~ -``` - -Usage ------ - -This bundle registers a `bazinga_geocoder.geocoder` service which is an instance -of `Geocoder`. You'll be able to do whatever you want with it but be sure to -configure at least **one provider** first. - -**NOTE:** When using `Request::getClientIp()` with Symfony 2.1+, ensure you have -a trusted proxy set in your `config.yml`: - -``` yaml -# app/config/config.yml -framework: - trusted_proxies: ['127.0.0.1'] - # ... -``` - -### Killer Feature - -You can fake the `REMOTE_ADDR` HTTP parameter through this bundle in order to get -information in your development environment, for instance: - -``` php -container - ->get('bazinga_geocoder.geocoder') - ->using('yahoo') - ->geocode($request->server->get('REMOTE_ADDR')); - - // Find the 5 nearest objects (15km) from the current user. - $objects = ObjectQuery::create() - ->filterByDistanceFrom($result->getLatitude(), $result->getLongitude(), 15) - ->limit(5) - ->find(); - - return array( - 'geocoded' => $result, - 'nearest_objects' => $objects - ); - } -``` - -In the example, we'll retrieve information from the user's IP address, and 5 -objects nears him. -But it won't work on your local environment, that's why this bundle provides -an easy way to fake this behavior by using a `fake_ip` configuration. - -``` yaml -# app/config/config_dev.yml -bazinga_geocoder: - fake_ip: 123.345.643.133 -``` - -If set, the parameter will replace the `REMOTE_ADDR` value by the given one. - - -Additionally if it interferes with your current -listeners, You can set up different fake ip listener priority. - - -``` yaml -# app/config/config_dev.yml -bazinga_geocoder: - fake_ip: - ip: 123.345.643.133 - priority: 128 -``` - -### Dumpers - -If you need to dump your geocoded data to a specific format, you can use the -__Dumper__ component. The following dumper's are supported: - - * Geojson - * GPX - * KMP - * WKB - * WKT - -Here is an example: - -```php -container - ->get('bazinga_geocoder.geocoder') - ->geocode($request->server->get('REMOTE_ADDR')); - - $body = $this->container - ->get('bazinga_geocoder.dumper_manager') - ->get('geojson') - ->dump($result); - - $response = new Response(); - $response->setContent($body); - - return $response; -} -``` - -To register a new dumper, you must tag it with _geocoder.dumper_. -Geocoder detect and register it automatically. - -A little example: - -```xml - - - -``` - -### Cache Provider - -Sometimes you have to cache the results from a provider. For this case the bundle provides -a cache provider. The cache provider wraps another provider and delegate all calls -to this provider and cache the return value. - -__Configuration example:__ - -```yaml -# app/config/config*.yml - -services: - acme_cache_adapter: - class: "Doctrine\Common\Cache\ApcCache" - -bazinga_geocoder: - providers: - cache: - adapter: acme_cache_adapter - provider: google_maps - google_maps: ~ -``` - -> Tip: If you want to configure the cache adapter, -> we recommend the [liip/doctrine-cache-bundle](https://github.com/liip/LiipDoctrineCacheBundle.git). - - -### Symfony2 Profiler Integration - -Geocoder bundle additionally integrates with Symfony2 profiler. You can -check number of queries executed by each provider, total execution time -and geocoding results. - -![Example -Toolbar](https://raw.github.com/willdurand/BazingaGeocoderBundle/master/Resources/doc/toolbar.png) - - -Reference Configuration ------------------------ - -You MUST define the providers you want to use in your configuration. Some of -them need information (API key for instance). - -You'll find the reference configuration below: - -``` yaml -# app/config/config*.yml - -bazinga_geocoder: - fake_ip: - enabled: true - ip: ~ - priority: 0 - adapter: - class: ~ - providers: - bing_maps: - api_key: ~ # Required - locale: ~ - cache: - adapter: ~ # Required - provider: ~ # Required - locale: ~ - lifetime: 86400 - ip_info_db: - api_key: ~ # Required - yahoo: - api_key: ~ # Required - locale: ~ - cloudmade: - api_key: ~ # Required - google_maps: - locale: ~ - region: ~ - use_ssl: false - google_maps_business: - client_id: ~ # Required - api_key: ~ - region: ~ - use_ssl: false - openstreetmaps: - locale: ~ - host_ip: [] - geoip: [] - free_geo_ip: [] - mapquest: [] - oiorest: [] - geocoder_ca: [] - geocoder_us: [] - ign_openls: - api_key: ~ # Required - data_science_toolkit: [] - yandex: - locale: ~ - toponym: ~ - geo_ips: - api_key: ~ - geo_plugin: [] - maxmind: - api_key: ~ # Required - maxmind_binary: - binary_file: ~ # Required - open_flag: ~ - chain: - providers: [] -``` - - -Testing -------- - -Setup the test suite using [Composer](http://getcomposer.org/): - - $ composer install --dev - -Run it using PHPUnit: - - $ phpunit diff --git a/Resources/doc/toolbar.png b/Resources/doc/toolbar.png deleted file mode 100644 index c86e12b3..00000000 Binary files a/Resources/doc/toolbar.png and /dev/null differ diff --git a/Resources/public/images/geocoder_marker.png b/Resources/public/images/geocoder_marker.png deleted file mode 100644 index c2d89a74..00000000 Binary files a/Resources/public/images/geocoder_marker.png and /dev/null differ diff --git a/Resources/views/Collector/geocoder.html.twig b/Resources/views/Collector/geocoder.html.twig deleted file mode 100644 index 6284c499..00000000 --- a/Resources/views/Collector/geocoder.html.twig +++ /dev/null @@ -1,78 +0,0 @@ -{% extends 'WebProfilerBundle:Profiler:layout.html.twig' %} - -{% block toolbar %} - {% set icon %} - - {{ collector.requestscount }} - {% endset %} - {% set text %} -
- Requests - {{ collector.requestsCount }} -
-
- Total time - {{ '%0.2f'|format(collector.time) }} ms -
- {% endset %} - {% include 'WebProfilerBundle:Profiler:toolbar_item.html.twig' with { 'link': profiler_url } %} -{% endblock %} - -{% block menu %} - - Geocoder - Geocoder - - {{ collector.requestsCount }} - {{ '%0.0f'|format(collector.time) }} ms - - -{% endblock %} - -{% block panel %} -

Geocoder Requests

- - {% if not collector.requestsCount %} -

No requests.

- {% else %} - - {% endif %} - - -{% endblock %} diff --git a/Tests/Command/GeocodeCommandTest.php b/Tests/Command/GeocodeCommandTest.php deleted file mode 100644 index f197285c..00000000 --- a/Tests/Command/GeocodeCommandTest.php +++ /dev/null @@ -1,60 +0,0 @@ - - */ -class GeocoderCommandTest extends \PHPUnit_Framework_TestCase -{ - private static $address = '10 rue Gambetta, Paris, France'; - - public function testExecute() - { - $geocoder = $this->getMock('Geocoder\\Geocoder'); - - $geocoder->expects($this->once()) - ->method('geocode') - ->with(self::$address) - ->will($this->returnValue(new Geocoded())); - - $container = $this->getMock('Symfony\\Component\\DependencyInjection\\Container'); - $container->expects($this->once()) - ->method('get') - ->with('bazinga_geocoder.geocoder') - ->will($this->returnValue($geocoder)); - - $kernel = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Kernel') - ->disableOriginalConstructor() - ->getMock(); - - $kernel->expects($this->once()) - ->method('getContainer') - ->will($this->returnValue($container)); - - $app = new Application($kernel); - $app->add(new GeocodeCommand()); - - $command = $app->find('geocoder:geocode'); - - $tester = new CommandTester($command); - $tester->execute(array( - 'command' => 'geocoder:geocode', - 'address' => self::$address, - )); - } -} diff --git a/Tests/DependencyInjection/BazingaGeocoderExtensionTest.php b/Tests/DependencyInjection/BazingaGeocoderExtensionTest.php deleted file mode 100644 index cf55a2be..00000000 --- a/Tests/DependencyInjection/BazingaGeocoderExtensionTest.php +++ /dev/null @@ -1,103 +0,0 @@ - - */ -class BazingaGeocoderExtensionTest extends \PHPUnit_Framework_TestCase -{ - public function testLoad() - { - $configs = Yaml::parse(file_get_contents(__DIR__.'/Fixtures/config.yml')); - $container = new ContainerBuilder(); - $extension = new BazingaGeocoderExtension(); - - $container->setParameter('fixtures_dir', __DIR__ . '/Fixtures'); - - $container->set('doctrine.apc.cache', new ArrayCache()); - - $container->addCompilerPass(new AddDumperPass()); - $container->addCompilerPass(new AddProvidersPass()); - $container->addCompilerPass(new LoggablePass()); - - $extension->load($configs, $container); - $container->compile(); - - $this->assertInstanceOf( - 'Bazinga\Bundle\GeocoderBundle\EventListener\FakeRequestListener', - $container->get('bazinga_geocoder.event_listener.fake_request') - ); - $this->assertNotNull( - $container->get('bazinga_geocoder.event_listener.fake_request')->getFakeIp() - ); - - $dumperManager = $container->get('bazinga_geocoder.dumper_manager'); - foreach (array('geojson', 'gpx', 'kmp', 'wkb', 'wkt') as $name) { - $this->assertTrue($dumperManager->has($name)); - } - - $geocoder = $container->get('bazinga_geocoder.geocoder'); - $providers = $geocoder->getProviders(); - foreach (array( - 'bing_maps' => 'Geocoder\\Provider\\BingMapsProvider', - 'cache' => 'Bazinga\\Bundle\\GeocoderBundle\\Provider\\CacheProvider', - 'ip_info_db' => 'Geocoder\\Provider\\IpInfoDbProvider', - 'yahoo' => 'Geocoder\\Provider\\YahooProvider', - 'cloudmade' => 'Geocoder\\Provider\\CloudMadeProvider', - 'google_maps' => 'Geocoder\\Provider\\GoogleMapsProvider', - 'google_maps_business' => 'Geocoder\\Provider\\GoogleMapsBusinessProvider', - 'openstreetmaps' => 'Geocoder\\Provider\\OpenStreetMapsProvider', - 'host_ip' => 'Geocoder\\Provider\\HostIpProvider', - 'free_geo_ip' => 'Geocoder\\Provider\\FreeGeoIpProvider', - 'map_quest' => 'Geocoder\\Provider\\MapQuestProvider', - 'oio_rest' => 'Geocoder\\Provider\\OIORestProvider', - 'geocoder_ca' => 'Geocoder\\Provider\\GeocoderCaProvider', - 'geocoder_us' => 'Geocoder\\Provider\\GeocoderUsProvider', - 'ign_openls' => 'Geocoder\\Provider\\IGNOpenLSProvider', - 'data_science_toolkit' => 'Geocoder\\Provider\\DataScienceToolkitProvider', - 'yandex' => 'Geocoder\\Provider\\YandexProvider', - 'geo_ips' => 'Geocoder\\Provider\\GeoIpsProvider', - 'geo_plugin' => 'Geocoder\\Provider\\GeoPluginProvider', - 'maxmind' => 'Geocoder\\Provider\\MaxmindProvider', - 'chain' => 'Geocoder\\Provider\\ChainProvider', - 'maxmind_binary' => 'Geocoder\\Provider\\MaxmindBinaryProvider', - ) as $name => $class) { - $this->assertInstanceOf($class, $providers[$name], sprintf('-> Assert that %s is instance of %s', $name, $class)); - } - } - - public function testLoadingFakeIpOldWay() - { - $configs = Yaml::parse(file_get_contents(__DIR__.'/Fixtures/old_fake_ip.yml')); - $container = new ContainerBuilder(); - $extension = new BazingaGeocoderExtension(); - - $container->setParameter('fixtures_dir', __DIR__ . '/Fixtures'); - - $container->set('doctrine.apc.cache', new ArrayCache()); - - $container->addCompilerPass(new AddDumperPass()); - $container->addCompilerPass(new AddProvidersPass()); - - $extension->load($configs, $container); - $container->compile(); - - $this->assertInstanceOf( - 'Bazinga\Bundle\GeocoderBundle\EventListener\FakeRequestListener', - $container->get('bazinga_geocoder.event_listener.fake_request') - ); - - $this->assertNotNull( - $container->get('bazinga_geocoder.event_listener.fake_request')->getFakeIp() - ); - } -} diff --git a/Tests/DependencyInjection/Compiler/AddDumperPassTest.php b/Tests/DependencyInjection/Compiler/AddDumperPassTest.php deleted file mode 100644 index 52a957be..00000000 --- a/Tests/DependencyInjection/Compiler/AddDumperPassTest.php +++ /dev/null @@ -1,39 +0,0 @@ - - */ -class AddDumperPassTest extends \PHPUnit_Framework_TestCase -{ - public function testProcess() - { - $builder = new ContainerBuilder; - $builder->setDefinition('bazinga_geocoder.dumper_manager', new Definition('Bazinga\Bundle\GeocoderBundle\DumperManager')); - - $dumper = new Definition('Geocoder\Dumper\GeoJsonDumper'); - $dumper->addTag('geocoder.dumper', array('alias' => 'geojson')); - - $builder->setDefinition('bazinga_geocoder.dumper.geojson', $dumper); - - $compiler = new AddDumperPass(); - $compiler->process($builder); - - $manager = $builder->get('bazinga_geocoder.dumper_manager'); - - $this->assertTrue($manager->has('geojson')); - } - - public function testProcessWithoutManager() - { - $builder = new ContainerBuilder(); - $compiler = new AddDumperPass; - - $this->assertNull($compiler->process($builder)); - } -} diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php deleted file mode 100644 index 9ec1f488..00000000 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ /dev/null @@ -1,24 +0,0 @@ - - */ -class ConfigurationTest extends \PHPUnit_Framework_TestCase -{ - public function testGetConfigTreeBuilder() - { - $config = Yaml::parse(file_get_contents(__DIR__.'/Fixtures/config.yml')); - - $configuration = new Configuration(); - $treeBuilder = $configuration->getConfigTreeBuilder(); - $processor = new Processor; - - $config = $processor->process($treeBuilder->buildTree(), $config); - } -} diff --git a/Tests/DependencyInjection/EventListener/FakeRequestListenerTest.php b/Tests/DependencyInjection/EventListener/FakeRequestListenerTest.php deleted file mode 100644 index 13a5dc48..00000000 --- a/Tests/DependencyInjection/EventListener/FakeRequestListenerTest.php +++ /dev/null @@ -1,27 +0,0 @@ - - */ -class FakeRequestListenerTest extends \PHPUnit_Framework_TestCase -{ - public function testOnKernelRequest() - { - $listener = new FakeRequestListener('33.33.33.1'); - - $kernel = $this->getMock('Symfony\\Component\\HttpKernel\\HttpKernelInterface'); - $request = new Request(); - $event = new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); - - $listener->onKernelRequest($event); - - $this->assertEquals('33.33.33.1', $request->server->get('REMOTE_ADDR')); - } -} diff --git a/Tests/DependencyInjection/Fixtures/config.yml b/Tests/DependencyInjection/Fixtures/config.yml deleted file mode 100644 index c4d2ce5d..00000000 --- a/Tests/DependencyInjection/Fixtures/config.yml +++ /dev/null @@ -1,54 +0,0 @@ -bazinga_geocoder: - fake_ip: - ip: 33.33.33.11 - priority: 128 - adapter: - class: Geocoder\HttpAdapter\CurlHttpAdapter - providers: - bing_maps: - api_key: 123 - locale: en_US - cache: - adapter: doctrine.apc.cache - provider: bing_maps - locale: en_US - lifetime: 86400 - ip_info_db: - api_key: 123 - yahoo: - api_key: 123 - locale: en_US - cloudmade: - api_key: 123 - google_maps: - locale: en_US - region: en_US - google_maps_business: - client_id: 123 - api_key: 123 - locale: en_US - region: en_US - openstreetmaps: - locale: en_US - host_ip: ~ - # geoip: ~ - free_geo_ip: ~ - mapquest: ~ - oiorest: ~ - geocoder_ca: ~ - geocoder_us: ~ - ign_openls: - api_key: 123 - data_science_toolkit: ~ - yandex: - locale: en-US - toponym: locality - geo_ips: - api_key: 123 - geo_plugin: ~ - maxmind: - api_key: 123 - maxmind_binary: - binary_file: %fixtures_dir%/maxmind.dat - chain: - providers: [free_geo_ip, host_ip] diff --git a/Tests/DependencyInjection/Fixtures/old_fake_ip.yml b/Tests/DependencyInjection/Fixtures/old_fake_ip.yml deleted file mode 100644 index eb475972..00000000 --- a/Tests/DependencyInjection/Fixtures/old_fake_ip.yml +++ /dev/null @@ -1,2 +0,0 @@ -bazinga_geocoder: - fake_ip: 33.33.33.01 diff --git a/Tests/DumperManagerTest.php b/Tests/DumperManagerTest.php deleted file mode 100644 index 273451ca..00000000 --- a/Tests/DumperManagerTest.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ -class DumperManagerTest extends \PHPUnit_Framework_TestCase -{ - /** - * @var DumperManager - */ - private $manager; - - protected function setup() - { - $this->manager = new DumperManager; - } - - public function testSet() - { - $this->assertNull($this->manager->set('test', $this->getMock('Geocoder\\Dumper\\DumperInterface'))); - } - - public function testGet() - { - $dumper = $this->getMock('Geocoder\\Dumper\\DumperInterface'); - $this->manager->set('test', $dumper); - - $this->assertEquals($dumper, $this->manager->get('test')); - } - - /** - * @expectedException \RuntimeException - */ - public function testGetNotExistDumper() - { - $this->manager->get('not-exist'); - } - - public function testHas() - { - $this->assertFalse($this->manager->has('test')); - $this->manager->set('test', $this->getMock('Geocoder\\Dumper\\DumperInterface')); - $this->assertTrue($this->manager->has('test')); - } - - public function testRemove() - { - $this->manager->set('test', $this->getMock('Geocoder\\Dumper\\DumperInterface')); - $this->manager->remove('test'); - $this->assertFalse($this->manager->has('test')); - } - - /** - * @expectedException \RuntimeException - */ - public function testRemoveNotExistDumper() - { - $this->manager->remove('not-exist'); - } -} diff --git a/Tests/Provider/CacheProviderTest.php b/Tests/Provider/CacheProviderTest.php deleted file mode 100644 index 03653ae3..00000000 --- a/Tests/Provider/CacheProviderTest.php +++ /dev/null @@ -1,84 +0,0 @@ - - */ -class CacheProviderTest extends \PHPUnit_Framework_TestCase -{ - public function testGetGeocodedData() - { - $address = 'Paris, France'; - $coordinates = array('lat' => 48.857049,'lng' => 2.35223); - - $delegate = $this->getMock('Geocoder\\Provider\\ProviderInterface'); - $delegate->expects($this->once()) - ->method('getGeocodedData') - ->with($address) - ->will($this->returnValue($coordinates)); - - $cache = $this->getMock('Doctrine\\Common\\Cache\\Cache'); - $cache->expects($this->once()) - ->method('fetch') - ->with(crc32($address)) - ->will($this->returnValue(false)); - - $cache->expects($this->once()) - ->method('save') - ->with(crc32($address), serialize($coordinates), 0); - - $provider = new CacheProvider($cache, $delegate); - $this->assertEquals($coordinates, $provider->getGeocodedData($address)); - } - - public function testGetCachedGeocodedData() - { - $address = 'Paris, France'; - $coordinates = array('lat' => 48.857049,'lng' => 2.35223); - - $delegate = $this->getMock('Geocoder\\Provider\\ProviderInterface'); - $delegate->expects($this->once()) - ->method('getGeocodedData') - ->with($address) - ->will($this->returnValue($coordinates)); - - $provider = new CacheProvider($cache = new ArrayCache(), $delegate); - - $provider->getGeocodedData($address); - $provider->getGeocodedData($address); - - $this->assertTrue($cache->contains(crc32($address))); - } - - public function testGetReversedData() - { - $coordinates = array(48.857049, 2.35223); - - $delegate = $this->getMock('Geocoder\\Provider\\ProviderInterface'); - $delegate->expects($this->once()) - ->method('getReversedData') - ->with($coordinates) - ->will($this->returnValue('Paris, France')); - - $cache = new ArrayCache(); - - $provider = new CacheProvider($cache, $delegate); - - $this->assertEquals('Paris, France', $provider->getReversedData($coordinates)); - $this->assertEquals('Paris, France', $provider->getReversedData($coordinates)); - } - - public function testGetName() - { - $delegate = $this->getMock('Geocoder\\Provider\\ProviderInterface'); - $cache = $this->getMock('Doctrine\\Common\\Cache\\Cache'); - - $provider = new CacheProvider($cache, $delegate); - - $this->assertEquals('cache', $provider->getName()); - } -} diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php deleted file mode 100644 index 8493ff33..00000000 --- a/Tests/bootstrap.php +++ /dev/null @@ -1,11 +0,0 @@ -=3.3 supports SimpleCache thanks to `Symfony\Component\Cache\Simple\Psr6Cache` adapter. +Thus a service can be registered like so: + +```yaml +# config/services.yaml +app.simple_cache: + class: Symfony\Component\Cache\Simple\Psr6Cache + arguments: ['@app.cache.acme'] +``` + +Then configure the framework and the bundle: + +```yaml +# config/packages/cache.yaml +framework: + cache: + app: cache.adapter.redis + pools: + app.cache.acme: + adapter: cache.app + default_lifetime: 600 + +# config/packages/bazinga_geocoder.yaml +bazinga_geocoder: + providers: + my_google_maps: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory + cache: 'app.simple_cache' + cache_lifetime: 3600 + cache_precision: 4 +``` + +For older Symfony version, you can use a bridge between PSR-6 and PSR-16. Install the +[bridge](https://github.com/php-cache/simple-cache-bridge) by: + +```bash +composer require cache/simple-cache-bridge +``` + +Then, in the service declaration you can use `Cache\Bridge\SimpleCache\SimpleCacheBridge` instead of `Symfony\Component\Cache\Simple\Psr6Cache`. diff --git a/doc/custom-provider.md b/doc/custom-provider.md new file mode 100644 index 00000000..abc1cfb5 --- /dev/null +++ b/doc/custom-provider.md @@ -0,0 +1,49 @@ +# Registering Your Own Providers + +*[<< Back to documentation index](/doc/index.md)* + +If you want to use your own provider in your application, create a service, and tag it as `bazinga_geocoder.provider`: + +```xml + + + + +``` + +The bundle will automatically register your provider into the`Geocoder\ProviderAggregator` service. However, it will not +show up the web profiler because it is not registered with the [PluginProvider](/doc/plugins.md). + +If you want your provider to show up the web profiler you have to create a custom factory for your provider. + +```php +namespace Acme\Demo\Geocoder\Factory; + +use Bazinga\GeocoderBundle\ProviderFactory\AbstractFactory; +use Acme\Demo\Geocoder\Provider\MyProvider; +use Acme\Demo\Service\Foo; + +final class MyFactory extends AbstractFactory +{ + private $fooService; + + public function __construct(Foo $service) + { + $this->someService = $service; + } + + protected function getProvider(array $config) + { + return new MyProvider($this->fooService); + } +} +``` + +```yaml +# config/packages/bazinga_geocoder.yaml +bazinga_geocoder: + providers: + acme: + factory: Acme\Demo\Geocoder\Factory\MyFactory + aliases: ['acme_demo.geocoder.my_provider'] +``` diff --git a/doc/doctrine.md b/doc/doctrine.md new file mode 100644 index 00000000..83c5afa1 --- /dev/null +++ b/doc/doctrine.md @@ -0,0 +1,135 @@ +# Doctrine annotation support + +*[<< Back to documentation index](/doc/index.md)* + +Wouldn't it be great if you could automatically save the coordinates of a users +address every time it is updated? Wait not more here is the feature you been always +wanted. + +First of all, update your entity: + +```php + +use Bazinga\GeocoderBundle\Mapping\Annotations as Geocoder; + +/** + * @Geocoder\Geocodeable + */ +class User +{ + /** + * @Geocoder\Address + */ + private $address; + + /** + * @Geocoder\Latitude + */ + private $latitude; + + /** + * @Geocoder\Longitude + */ + private $longitude; +} +``` + +Instead of annotating a property, you can also annotate a getter: + +```php + +use Bazinga\GeocoderBundle\Mapping\Annotations as Geocoder; + +/** + * @Geocoder\Geocodeable + */ +class User +{ + /** + * @Geocoder\Latitude + */ + private $latitude; + + /** + * @Geocoder\Longitude + */ + private $longitude; + + /** + * @Geocoder\Address + */ + public function getAddress(): string + { + // Your code... + } +} +``` + +Secondly, register the Doctrine event listener and its dependencies in your `config/services.yaml` file. +You have to indicate which provider to use to reverse geocode the address. Here we use `acme` provider we declared in bazinga_geocoder configuration earlier. + +```yaml + Bazinga\GeocoderBundle\Mapping\Driver\AnnotationDriver: + class: Bazinga\GeocoderBundle\Mapping\Driver\AnnotationDriver + arguments: + - '@annotations.reader' + + Bazinga\GeocoderBundle\Doctrine\ORM\GeocoderListener: + class: Bazinga\GeocoderBundle\Doctrine\ORM\GeocoderListener + arguments: + - '@bazinga_geocoder.provider.acme' + - '@Bazinga\GeocoderBundle\Mapping\Driver\AnnotationDriver' + tags: + - { name: doctrine.event_listener, event: onFlush } +``` + +It is done! +Now you can use it: + +```php +$user = new User(); +$user->setAddress('Brandenburger Tor, Pariser Platz, Berlin'); + +$em->persist($event); +$em->flush(); + +echo $user->getLatitude(); // will output 52.516325 +echo $user->getLongitude(); // will output 13.377264 +``` + +## PHP 8 + +If you are using PHP 8, you can use [Attributes](https://www.php.net/manual/en/language.attributes.overview.php) in your entity: + +```php + +use Bazinga\GeocoderBundle\Mapping\Annotations as Geocoder; + +#[Geocoder\Geocodeable()] +class User +{ + #[Geocoder\Address()] + private $address; + + #[Geocoder\Latitude()] + private $latitude; + + #[Geocoder\Longitude()] + private $longitude; +} +``` + +Then update your service configuration to register the `AttributeDriver`: + +```yaml + Bazinga\GeocoderBundle\Mapping\Driver\AttributeDriver: + class: Bazinga\GeocoderBundle\Mapping\Driver\AttributeDriver + + Bazinga\GeocoderBundle\Doctrine\ORM\GeocoderListener: + class: Bazinga\GeocoderBundle\Doctrine\ORM\GeocoderListener + arguments: + - '@bazinga_geocoder.provider.acme' + - '@Bazinga\GeocoderBundle\Mapping\Driver\AttributeDriver' + tags: + - { name: doctrine.event_listener, event: onFlush } +``` diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 00000000..3868bf0c --- /dev/null +++ b/doc/index.md @@ -0,0 +1,390 @@ +BazingaGeocoderBundle +===================== + +Integration of the [**Geocoder**](http://github.com/geocoder-php/Geocoder) library into Symfony. + +Our documentation has the following sections: + +* [Index](index.md) (This page) +* [Public services](services.md) (Providers) +* [Registering Your Own Provider](custom-provider.md) +* [All about Cache](cache.md) +* [Plugins](plugins.md) +* [Doctrine support](doctrine.md) + +Table of contents +----------------- + +* [Installation](#installation) +* [Usage](#usage) + * [Chain providers](#chain-providers) + * [Fake local ip](#fake-local-ip) + * [Cache](#cache-results) + * [Dumpers](#dumpers) + * [Custom HTTP clients](#custom-http-clients) +* [Reference Configuration](#reference-configuration) +* [Backwards compatibility](#backwards-compatibility) +* [Testing](#testing) + +Installation +------------ + +To install this bundle you need to know how to [install the geocoder and providers](https://github.com/geocoder-php/Geocoder#installation) +and then you may just install the bundle like normal: + +```bash +composer require willdurand/geocoder-bundle:^5.0 +``` + +If you don't use [Symfony Flex](https://symfony.com/doc/current/setup/flex.html), you must enable the bundle manually in the application: + +```php +// config/bundles.php +// in older Symfony apps, enable the bundle in app/AppKernel.php +return [ + // ... + Bazinga\GeocoderBundle\BazingaGeocoderBundle::class => ['all' => true], +]; +``` + +Usage +----- + +The bundle helps you register your providers and to enable profiling support. To +configure a provider you must use a `ProviderFactory`. See the following example +using Google Maps. + +```yaml +bazinga_geocoder: + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory +``` + +This will create a service named `bazinga_geocoder.provider.acme` which is a +`GoogleMapsProvider`. + +You can also configure **all ``ProviderFactories``** to adjust the behavior of the +provider. + +```yaml +bazinga_geocoder: + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory + cache: 'any.psr16.service' + cache_lifetime: 3600 + aliases: + - my_geocoder +``` + +This will create a service named `my_geocoder` that caches the responses for one +hour. + +**Most ``ProviderFactories``** do also take an array with options. This is usually +parameters to the constructor of the provider. In the example of Google Maps: + +```yaml +bazinga_geocoder: + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory + options: + httplug_client: '@httplug.client' # When using HTTPlugBundle + region: 'Sweden' + api_key: 'xxyy' +``` + +### Chain providers + +Someone was thinking ahead here. Wouldn't it be nice if you could pass your request through different `ProviderFactories`? You can!! With the `ChainFactory`, see the configuration below. + +```yaml +bazinga_geocoder: + providers: + acme: + aliases: + - my_geocoder + cache: 'any.psr16.service' + cache_lifetime: 3600 + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory + options: + api_key: 'xxxx' + acme_ii: + aliases: + - my_geocoder_ii + factory: Bazinga\GeocoderBundle\ProviderFactory\TomTomFactory + options: + api_key: 'xxyy' + httplug_client: '@httplug.client' # When using HTTPlugBundle + region: 'Sweden' + chain: + factory: Bazinga\GeocoderBundle\ProviderFactory\ChainFactory + options: + services: ['@bazinga_geocoder.provider.acme', '@bazinga_geocoder.provider.acme_ii'] +``` + +The `services` key could also be as follows `services: ['@my_geocoder', '@my_geocoder_ii']`. Notice these are the values from the `aliases` key. + +### Autowiring Providers + +If you're using autowiring you can use bindings provided the bundle. + +```yaml +bazinga_geocoder: + providers: + googleMaps: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory +``` + +```php +googleMapsGeocoder = $googleMapsGeocoder; + } +} +``` + +Each configured provider has a binding in the following format: +`providerName + Geocoder`. + +In the example we configured provider name as `googleMaps` so the argument is `$googleMapsGeocoder`. + +### Fake local ip + +You can fake your local IP through this bundle in order to get location +information in your development environment, for instance: + +```php +/** + * @Template() + */ +public function indexAction(Request $request) +{ + // Retrieve information from the current user (by its IP address) + $result = $this->container + ->get('bazinga_geocoder.provider.acme') + ->geocodeQuery(GeocodeQuery::create($request->server->get('REMOTE_ADDR'))); + + // Find the 5 nearest objects (15km) from the current user. + $coords = $result->first()->getCoordinates(); + $objects = ObjectQuery::create() + ->filterByDistanceFrom($coords->getLatitude(), $coords->getLongitude(), 15) + ->limit(5) + ->find(); + + return array( + 'geocoded' => $result, + 'nearest_objects' => $objects + ); +} +``` + +In the example above, we'll retrieve information from the user's IP address, and 5 +objects nears him. +But it won't work on your local environment, that's why this bundle provides +an easy way to fake this behavior by using a `fake_ip` configuration. + +```yaml +# config/packages/bazinga_geocoder.yaml + +when@dev: + bazinga_geocoder: + fake_ip: 123.123.123.123 +``` + +If set, the parameter will replace all instances of "127.0.0.1" in your queries and replace them with the given one. +If you'd like to replace other ip instead of "127.0.0.1" (e.g. when using localhost inside a VM) you can set the optional `local_ip` parameter: + +```yaml +when@dev: + bazinga_geocoder: + fake_ip: + local_ip: 192.168.99.1 # default 127.0.0.1 + ip: 123.123.123.123 +``` + +You can also let [Faker](https://github.com/fakerphp/faker) generate fake ip for you. + +```yaml +when@dev: + bazinga_geocoder: + fake_ip: + use_faker: true # default false +``` + +### Cache Results + +Sometimes you have to cache the results from a provider. For this case the bundle provides +simple configuration. You only need to provide a service name for you SimpleCache (PSR-16) +service and you are good to go. + +```yaml +bazinga_geocoder: + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory + cache: 'any.psr16.service' + cache_lifetime: 3600 +``` + +Read more about cache [here](cache.md). + +### Dumpers + +If you need to dump your geocoded data to a specific format, you can use the +__Dumper__ component. The following dumper's are supported: + + * GeoArray + * GeoJson + * GPX + * KML + * WKB + * WKT + +Here is an example if you are using autowiring: + +```php +acmeGeocoder = $acmeGeocoder; + $this->geoJsonDumper = $dumper; + } + + public function geocodeAction(Request $request) + { + $result = $this->acmeGeocoder->geocodeQuery(GeocodeQuery::create($request->server->get('REMOTE_ADDR'))); + + $body = $this->geoJsonDumper->dump($result); + + return new JsonResponse($body); + } +} +``` + +Each dumper service if it implements `Geocoder\Dumper\Dumper` interface will be +tagged with `bazinga_geocoder.dumper` tag. Each dumper can be used with autowiring +providing the dumper class name as the argument. +Also If you want to inject all the tagged dumpers to your service you can provide +your service argument as: `!tagged bazinga_geocoder.dumper`. + +### Custom HTTP Clients + +The HTTP geocoder providers integrates with [HTTPlug](http://httplug.io/). It will give you all +the power of the HTTP client. You have to select which one you want to use and how +you want to configure it. + +Read their [usage page](http://docs.php-http.org/en/latest/httplug/users.html), you +may also be interested in checking out the [HTTPlugBundle](https://github.com/php-http/HttplugBundle). + +An example, if you want to use Guzzle6. + +```bash +composer require php-http/guzzle6-adapter php-http/message +``` + +Reference Configuration +----------------------- + +You'll find the reference configuration below: + +```yaml +# config/packages/bazinga_geocoder.yaml +bazinga_geocoder: + profiling: + enabled: ~ # Default is same as kernel.debug + fake_ip: + enabled: true + ip: null + providers: + # ... + acme: + factory: ~ # Required + cache: 'app.cache' + cache_lifetime: 3600 + cache_precision: 4 # Precision of the coordinates to cache. + limit: 5 + locale: 'sv' + logger: 'logger' + plugins: + - my_custom_plugin + aliases: + - acme + - acme_geocoder + options: + foo: bar + biz: baz + # ... + free_chain: + aliases: + - free_geo_chain + factory: Bazinga\GeocoderBundle\ProviderFactory\ChainFactory + options: + services: ['@acme', '@acme_ii'] +``` + +Backwards compatibility +----------------------- + +The BazingaGeocoderBundle is just a Symfony integration for Geocoder-PHP and it +does not have any classes which falls under the BC promise. The backwards compatibility +of the bundle is only the configuration and its values (and of course the behavior +of those values). + +The public service names (excluding the ones related to profiling/DataCollector) +falls under the backwards compatibility promise. + +Bottom line is, that you can trust that your configuration will not break and that +the services you use will still be working. + +Testing +------- + +Setup the test suite using [Composer](http://getcomposer.org/): + + +```bash +composer update +composer test +``` + +### Doctrine test + +There is also a test that tests the doctrine integration. It runs automatically on +[GitHub Actions](https://github.com/geocoder-php/BazingaGeocoderBundle/actions) but if you want to run it locally you must do the following. + +```bash +composer require phpunit/phpunit:^9.5 --no-update +composer update --prefer-source +wget https://phar.phpunit.de/phpunit-9.5.phar +php phpunit-9.5.phar --testsuit doctrine +``` + +**Important:** this command must be run with `--prefer-source`, otherwise the +`Doctrine\Tests\OrmTestCase` class won't be found. + diff --git a/doc/plugins.md b/doc/plugins.md new file mode 100644 index 00000000..79748fe1 --- /dev/null +++ b/doc/plugins.md @@ -0,0 +1,47 @@ +# Plugins + +*[<< Back to documentation index](/doc/index.md)* + +Plugins lets you modify or take actions the `Query` or the result. There are a few plugins from the `geocoder-php/plugin` +package like `LimitPlugin`, `LoggerPlugin` and `CachePlugin`. Some of them are supported in the configuration. + +```yaml +# config/packages/bazinga_geocoder.yaml +bazinga_geocoder: + fake_ip: + ip: '123.123.123.123' # Uses the FakeIpPlugin + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory + cache: 'app.cache' # Uses the CachePlugin + limit: 5 # Uses the LimitPlugin + locale: 'sv' # Uses the LocalePlugin + logger: 'logger' # Uses the LoggerPlugin +``` + +To use a any other plugins you must first register them as a service. Say you want to add some data to each query. You +may then use the `QueryDataPlugin`. + +```yaml +# config/services.yaml +services: + app.query_data_plugin: + class: Geocoder\Plugin\Plugin\QueryDataPlugin + arguments: + - ['foo': 'bar'] + - true +``` + +```yaml +# config/packages/bazinga_geocoder.yaml +bazinga_geocoder: + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory + plugins: + - 'app.query_data_plugin' +``` + +This will execute `$query = $query->withData('foo', 'bar');` on all queries executed by the acme provider. + +Read more about plugins at the [Geocoder's documentation](https://github.com/geocoder-php/Geocoder). diff --git a/doc/profiler-page.png b/doc/profiler-page.png new file mode 100644 index 00000000..5bf74611 Binary files /dev/null and b/doc/profiler-page.png differ diff --git a/doc/services.md b/doc/services.md new file mode 100644 index 00000000..22a2378f --- /dev/null +++ b/doc/services.md @@ -0,0 +1,53 @@ +# Public services + +*[<< Back to documentation index](/doc/index.md)* + +This is a list of our public services. They are all part of [BC promise](/doc/index.md#backwards-compatibility). + +### Provider Factories + +Here is a list of all provider factories and their options. + +| Service | Options | +| ------- | ------- | +| `Bazinga\GeocoderBundle\ProviderFactory\AlgoliaPlaceFactory` | httplug_client, api_key, app_id +| `Bazinga\GeocoderBundle\ProviderFactory\ArcGISOnlineFactory` | httplug_client, source_country +| `Bazinga\GeocoderBundle\ProviderFactory\BingMapsFactory` | httplug_client, api_key +| `Bazinga\GeocoderBundle\ProviderFactory\ChainFactory` | services +| `Bazinga\GeocoderBundle\ProviderFactory\FreeGeoIpFactory` | httplug_client, base_url +| `Bazinga\GeocoderBundle\ProviderFactory\GeoIP2Factory` | provider, database_filename, user_id, license_key, webservice_options, locales, provider_service +| `Bazinga\GeocoderBundle\ProviderFactory\GeoipFactory` | +| `Bazinga\GeocoderBundle\ProviderFactory\GeoIPsFactory` | httplug_client, api_key +| `Bazinga\GeocoderBundle\ProviderFactory\GeonamesFactory` | httplug_client, username +| `Bazinga\GeocoderBundle\ProviderFactory\GeoPluginFactory` | httplug_client +| `Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory` | httplug_client, api_key, region +| `Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsPlacesFactory` | httplug_client, api_key +| `Bazinga\GeocoderBundle\ProviderFactory\HereFactory` | httplug_client, app_id, app_code, use_cit +| `Bazinga\GeocoderBundle\ProviderFactory\HostIpFactory` | httplug_client +| `Bazinga\GeocoderBundle\ProviderFactory\IpInfoFactory` | httplug_client +| `Bazinga\GeocoderBundle\ProviderFactory\IpInfoDbFactory` | httplug_client, api_key, precision +| `Bazinga\GeocoderBundle\ProviderFactory\IpstackFactory` | httplug_client, api_key +| `Bazinga\GeocoderBundle\ProviderFactory\LocationIQFactory` | httplug_client, api_key +| `Bazinga\GeocoderBundle\ProviderFactory\MapboxFactory` | httplug_client, api_key, country, mode +| `Bazinga\GeocoderBundle\ProviderFactory\MapQuestFactory` | httplug_client, api_key, licensed +| `Bazinga\GeocoderBundle\ProviderFactory\MapzenFactory` | httplug_client, api_key +| `Bazinga\GeocoderBundle\ProviderFactory\MaxMindBinaryFactory` | dat_file, open_flag +| `Bazinga\GeocoderBundle\ProviderFactory\MaxMindFactory` | httplug_client, api_key, endpoint +| `Bazinga\GeocoderBundle\ProviderFactory\NominatimFactory` | httplug_client, root_url +| `Bazinga\GeocoderBundle\ProviderFactory\OpenCageFactory` | httplug_client, api_key +| `Bazinga\GeocoderBundle\ProviderFactory\OpenRouteServiceFactory` | httplug_client, api_key +| `Bazinga\GeocoderBundle\ProviderFactory\PickPointFactory` | httplug_client, api_key +| `Bazinga\GeocoderBundle\ProviderFactory\TomTomFactory` | httplug_client, api_key +| `Bazinga\GeocoderBundle\ProviderFactory\YandexFactory` | httplug_client, toponym + +### Services + +Except for the provider factories, here is a list of services this bundle exposes are: + +* `Geocoder\ProviderAggregator` +* `Geocoder\Dumper\GeoArray` +* `Geocoder\Dumper\GeoJson` +* `Geocoder\Dumper\Gpx` +* `Geocoder\Dumper\Kml` +* `Geocoder\Dumper\Wkb` +* `Geocoder\Dumper\Wkt` diff --git a/doc/toolbar.png b/doc/toolbar.png new file mode 100644 index 00000000..a243ed6a Binary files /dev/null and b/doc/toolbar.png differ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..4d7a09d4 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,196 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#1 \\$name of method Geocoder\\\\ProviderAggregator\\:\\:using\\(\\) expects string, mixed given\\.$#" + count: 1 + path: src/Command/GeocodeCommand.php + + - + message: "#^Parameter \\#1 \\$string of function strtolower expects string, string\\|null given\\.$#" + count: 1 + path: src/Command/GeocodeCommand.php + + - + message: "#^Parameter \\#1 \\$text of static method Geocoder\\\\Query\\\\GeocodeQuery\\:\\:create\\(\\) expects string, mixed given\\.$#" + count: 1 + path: src/Command/GeocodeCommand.php + + - + message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" + count: 3 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Cannot access offset 'aliases' on mixed\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Cannot access offset 'enabled' on mixed\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Cannot access offset 'factory' on mixed\\.$#" + count: 5 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Cannot access offset 'id' on mixed\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Cannot access offset 'options' on mixed\\.$#" + count: 4 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Cannot access offset 'reference' on mixed\\.$#" + count: 2 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Cannot call static method validate\\(\\) on mixed\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Cannot cast mixed to string\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Parameter \\#1 \\$alias of method Symfony\\\\Component\\\\DependencyInjection\\\\ContainerBuilder\\:\\:setAlias\\(\\) expects string, mixed given\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(mixed\\)\\: mixed\\)\\|null, Closure\\(string\\)\\: Symfony\\\\Component\\\\DependencyInjection\\\\Reference given\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Parameter \\#1 \\$factoryServiceId of static method Bazinga\\\\GeocoderBundle\\\\DependencyInjection\\\\Compiler\\\\FactoryValidatorPass\\:\\:addFactoryServiceId\\(\\) expects string, mixed given\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Parameter \\#1 \\$id of class Symfony\\\\Component\\\\DependencyInjection\\\\Reference constructor expects string, mixed given\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Parameter \\#1 \\$id of method Symfony\\\\Component\\\\DependencyInjection\\\\ContainerBuilder\\:\\:getDefinition\\(\\) expects string, mixed given\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Parameter \\#1 \\$object_or_class of function class_implements expects object\\|string, mixed given\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Parameter \\#1 \\$options of method Bazinga\\\\GeocoderBundle\\\\DependencyInjection\\\\BazingaGeocoderExtension\\:\\:findReferences\\(\\) expects array\\, mixed given\\.$#" + count: 2 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Parameter \\#1 \\$string of function ltrim expects string, mixed given\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Parameter \\#2 \\$config of method Bazinga\\\\GeocoderBundle\\\\DependencyInjection\\\\BazingaGeocoderExtension\\:\\:configureProviderPlugins\\(\\) expects array\\, mixed given\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Part \\$providerName \\(mixed\\) of encapsed string cannot be cast to string\\.$#" + count: 1 + path: src/DependencyInjection/BazingaGeocoderExtension.php + + - + message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\:\\:end\\(\\)\\.$#" + count: 2 + path: src/DependencyInjection/Configuration.php + + - + message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\:\\:variableNode\\(\\)\\.$#" + count: 1 + path: src/DependencyInjection/Configuration.php + + - + message: "#^Call to an undefined method Doctrine\\\\ORM\\\\Event\\\\OnFlushEventArgs\\:\\:getEntityManager\\(\\)\\.$#" + count: 1 + path: src/Doctrine/ORM/GeocoderListener.php + + - + message: "#^Cannot call method getLatitude\\(\\) on Geocoder\\\\Model\\\\Coordinates\\|null\\.$#" + count: 1 + path: src/Doctrine/ORM/GeocoderListener.php + + - + message: "#^Cannot call method getLongitude\\(\\) on Geocoder\\\\Model\\\\Coordinates\\|null\\.$#" + count: 1 + path: src/Doctrine/ORM/GeocoderListener.php + + - + message: "#^Method Bazinga\\\\GeocoderBundle\\\\Mapping\\\\Driver\\\\AnnotationDriver\\:\\:getReflection\\(\\) return type with generic class ReflectionClass does not specify its types\\: T$#" + count: 1 + path: src/Mapping/Driver/AnnotationDriver.php + + - + message: "#^Method Bazinga\\\\GeocoderBundle\\\\Mapping\\\\Driver\\\\AttributeDriver\\:\\:getReflection\\(\\) return type with generic class ReflectionClass does not specify its types\\: T$#" + count: 1 + path: src/Mapping/Driver/AttributeDriver.php + + - + message: "#^Parameter \\#1 \\$text of method Geocoder\\\\Query\\\\GeocodeQuery\\:\\:withText\\(\\) expects string, string\\|null given\\.$#" + count: 1 + path: src/Plugin/FakeIpPlugin.php + + - + message: "#^Parameter \\#2 \\$replace of function str_replace expects array\\|string, string\\|null given\\.$#" + count: 1 + path: src/Plugin/FakeIpPlugin.php + + - + message: "#^Parameter \\#1 \\$accountId of class GeoIp2\\\\WebService\\\\Client constructor expects int, int\\|null given\\.$#" + count: 1 + path: src/ProviderFactory/GeoIP2Factory.php + + - + message: "#^Parameter \\#1 \\$filename of class GeoIp2\\\\Database\\\\Reader constructor expects string, string\\|null given\\.$#" + count: 1 + path: src/ProviderFactory/GeoIP2Factory.php + + - + message: "#^Parameter \\#1 \\$geoIpProvider of class Geocoder\\\\Provider\\\\GeoIP2\\\\GeoIP2Adapter constructor expects GeoIp2\\\\ProviderInterface, GeoIp2\\\\ProviderInterface\\|null given\\.$#" + count: 1 + path: src/ProviderFactory/GeoIP2Factory.php + + - + message: "#^Parameter \\#2 \\$licenseKey of class GeoIp2\\\\WebService\\\\Client constructor expects string, string\\|null given\\.$#" + count: 1 + path: src/ProviderFactory/GeoIP2Factory.php + + - + message: "#^Class Geocoder\\\\Provider\\\\Geoip\\\\Geoip not found\\.$#" + count: 1 + path: src/ProviderFactory/GeoipFactory.php + + - + message: "#^Instantiated class Geocoder\\\\Provider\\\\Geoip\\\\Geoip not found\\.$#" + count: 1 + path: src/ProviderFactory/GeoipFactory.php + + - + message: "#^Method Bazinga\\\\GeocoderBundle\\\\ProviderFactory\\\\GeoipFactory\\:\\:getProvider\\(\\) should return Geocoder\\\\Provider\\\\Provider but returns Geocoder\\\\Provider\\\\Geoip\\\\Geoip\\.$#" + count: 1 + path: src/ProviderFactory/GeoipFactory.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..7b4f667f --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,10 @@ +includes: + - phpstan-baseline.neon + +parameters: + bootstrapFiles: + - vendor/bin/.phpunit/phpunit/vendor/autoload.php + paths: + - src/ + #- tests/ + level: 9 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d202effc..5d36cb52 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,30 +1,31 @@ - + + + + src + + + - - ./Tests + + ./tests - - - . - - ./Resources - ./Tests - ./vendor - - - - \ No newline at end of file + + + + diff --git a/BazingaGeocoderBundle.php b/src/BazingaGeocoderBundle.php similarity index 55% rename from BazingaGeocoderBundle.php rename to src/BazingaGeocoderBundle.php index 5624c2e3..e9aba853 100644 --- a/BazingaGeocoderBundle.php +++ b/src/BazingaGeocoderBundle.php @@ -1,6 +1,8 @@ */ class BazingaGeocoderBundle extends Bundle { /** - * {@inheritdoc} + * @return void */ public function build(ContainerBuilder $container) { parent::build($container); + $container->addCompilerPass(new ProfilerPass()); $container->addCompilerPass(new AddProvidersPass()); - $container->addCompilerPass(new AddDumperPass()); - $container->addCompilerPass(new LoggablePass()); + $container->addCompilerPass(new FactoryValidatorPass()); + } + + public function getPath(): string + { + return \dirname(__DIR__); } } diff --git a/Command/GeocodeCommand.php b/src/Command/GeocodeCommand.php similarity index 64% rename from Command/GeocodeCommand.php rename to src/Command/GeocodeCommand.php index a0cadece..e4de4837 100644 --- a/Command/GeocodeCommand.php +++ b/src/Command/GeocodeCommand.php @@ -1,6 +1,8 @@ */ -class GeocodeCommand extends ContainerAwareCommand +#[AsCommand(name: 'geocoder:geocode', description: 'Geocode an address or a ip address')] +class GeocodeCommand extends Command { + private ProviderAggregator $geocoder; + + public function __construct(ProviderAggregator $geocoder) + { + $this->geocoder = $geocoder; + + parent::__construct(); + } + /** - * {@inheritDoc} + * @return void */ protected function configure() { $this - ->setName('geocoder:geocode') - ->setDescription('Geocode an address or a ip address') ->addArgument('address', InputArgument::REQUIRED, 'The address') ->addOption('provider', null, InputOption::VALUE_OPTIONAL) - ->setHelp(<<setHelp(<<<'HELP' The geocoder:geocoder command will fetch the latitude and longitude from the given address. You can force a provider with the "provider" option. -php app/console geocoder:geocoder "Eiffel Tower" --provider=yahoo +php bin/console geocoder:geocoder "Eiffel Tower" --provider=yahoo HELP ); } - /** - * {@inheritDoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - /** @var $geocoder \Geocoder\Geocoder */ - $geocoder = $this->getContainer()->get('bazinga_geocoder.geocoder'); - if ($input->getOption('provider')) { - $geocoder->using($input->getOption('provider')); + $this->geocoder->using($input->getOption('provider')); } - $result = $geocoder->geocode($input->getArgument('address')); - $data = $result->toArray(); + $results = $this->geocoder->geocodeQuery(GeocodeQuery::create($input->getArgument('address'))); + $data = $results->first()->toArray(); $max = 0; @@ -77,9 +84,11 @@ protected function execute(InputInterface $input, OutputInterface $output) is_array($value) ? json_encode($value) : $value )); } + + return 0; } - private function humanize($text) + private function humanize(string $text): string { $text = preg_replace('/([A-Z][a-z]+)|([A-Z][A-Z]+)|([^A-Za-z ]+)/', ' \1', $text); diff --git a/src/DataCollector/GeocoderDataCollector.php b/src/DataCollector/GeocoderDataCollector.php new file mode 100644 index 00000000..0a8ec829 --- /dev/null +++ b/src/DataCollector/GeocoderDataCollector.php @@ -0,0 +1,122 @@ + + */ +class GeocoderDataCollector extends DataCollector +{ + /** + * @var ProfilingPlugin[] + */ + private array $instances = []; + + public function __construct() + { + $this->data['queries'] = []; + $this->data['providers'] = []; + } + + /** + * @return void + */ + public function reset() + { + $this->instances = []; + $this->data['queries'] = []; + $this->data['providers'] = []; + } + + /** + * @return void + */ + public function collect(Request $request, Response $response, ?\Throwable $exception = null) + { + if (!empty($this->data['queries'])) { + // To avoid collection more that once. + return; + } + + $instances = $this->instances; + + foreach ($instances as $instance) { + foreach ($instance->getQueries() as $query) { + $query['query'] = $this->cloneVar($query['query']); + $query['result'] = $this->cloneVar($query['result']); + $this->data['queries'][] = $query; + } + } + } + + /** + * Returns an array of collected requests. + * + * @return list + */ + public function getQueries(): array + { + return $this->data['queries']; + } + + /** + * Returns the execution time of all collected requests in seconds. + */ + public function getTotalDuration(): float + { + $time = 0; + foreach ($this->data['queries'] as $command) { + $time += $command['duration']; + } + + return $time; + } + + /** + * @return string[] + */ + public function getProviders(): array + { + return $this->data['providers']; + } + + /** + * @return list + */ + public function getProviderQueries(string $provider): array + { + return array_filter($this->data['queries'], static function ($data) use ($provider) { + return $data['providerName'] === $provider; + }); + } + + /** + * @return void + */ + public function addInstance(ProfilingPlugin $instance) + { + $this->instances[] = $instance; + $this->data['providers'][] = $instance->getName(); + } + + public function getName(): string + { + return 'geocoder'; + } +} diff --git a/src/DependencyInjection/BazingaGeocoderExtension.php b/src/DependencyInjection/BazingaGeocoderExtension.php new file mode 100644 index 00000000..9ea854aa --- /dev/null +++ b/src/DependencyInjection/BazingaGeocoderExtension.php @@ -0,0 +1,227 @@ +. + */ +class BazingaGeocoderExtension extends Extension +{ + /** + * @param array $configs + * + * @return void + */ + public function load(array $configs, ContainerBuilder $container) + { + $processor = new Processor(); + $configuration = $this->getConfiguration($configs, $container); + $config = $processor->processConfiguration($configuration, $configs); + + $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../../config')); + $loader->load('services.yml'); + + if (true === $config['profiling']['enabled']) { + $loader->load('profiling.yml'); + } + + if ($config['fake_ip']['enabled']) { + $definition = $container->getDefinition(FakeIpPlugin::class); + $definition->replaceArgument(0, $config['fake_ip']['local_ip']); + $definition->replaceArgument(1, $config['fake_ip']['ip']); + $definition->replaceArgument(2, $config['fake_ip']['use_faker']); + + if ($config['fake_ip']['use_faker'] && !class_exists(Generator::class)) { + throw new \LogicException('To enable this option, you must install fakerphp/faker package.'); + } + } else { + $container->removeDefinition(FakeIpPlugin::class); + } + + $this->loadProviders($container, $config); + + $container->registerForAutoconfiguration(Dumper::class) + ->addTag('bazinga_geocoder.dumper'); + } + + /** + * @param array $config + * + * @return void + */ + private function loadProviders(ContainerBuilder $container, array $config) + { + foreach ($config['providers'] as $providerName => $providerConfig) { + try { + $factoryService = $container->getDefinition($providerConfig['factory']); + $factoryClass = $factoryService->getClass() ?: $providerConfig['factory']; + if (!$this->implementsProviderFactory($factoryClass)) { + throw new \LogicException(sprintf('Provider factory "%s" must implement ProviderFactoryInterface', $providerConfig['factory'])); + } + // See if any option has a service reference + $providerConfig['options'] = $this->findReferences($providerConfig['options']); + $factoryClass::validate($providerConfig['options'], $providerName); + } catch (ServiceNotFoundException $e) { + // Assert: We are using a custom factory. If invalid config, it will be caught in FactoryValidatorPass + $providerConfig['options'] = $this->findReferences($providerConfig['options']); + FactoryValidatorPass::addFactoryServiceId($providerConfig['factory']); + } + + $serviceId = 'bazinga_geocoder.provider.'.$providerName; + $plugins = $this->configureProviderPlugins($container, $providerConfig, $serviceId); + + $def = $container->register($serviceId, PluginProvider::class) + ->setFactory([PluginProviderFactory::class, 'createPluginProvider']) + ->addArgument($plugins) + ->addArgument(new Reference($providerConfig['factory'])) + ->addArgument($providerConfig['options']); + + $def->addTag('bazinga_geocoder.provider'); + foreach ($providerConfig['aliases'] as $alias) { + $container->setAlias($alias, $serviceId); + } + + $container->registerAliasForArgument($serviceId, Provider::class, "{$providerName}Geocoder"); + } + } + + /** + * Configure plugins for a client. + * + * @param array $config + * + * @return Reference[] + */ + public function configureProviderPlugins(ContainerBuilder $container, array $config, string $providerServiceId): array + { + $plugins = []; + foreach ($config['plugins'] as $plugin) { + if ($plugin['reference']['enabled']) { + $plugins[] = $plugin['reference']['id']; + } + } + + if ($container->has(FakeIpPlugin::class)) { + $plugins[] = FakeIpPlugin::class; + } + + if (isset($config['cache']) || isset($config['cache_lifetime']) || isset($config['cache_precision'])) { + $cacheLifetime = isset($config['cache_lifetime']) ? (int) $config['cache_lifetime'] : null; + + if (null === $cacheServiceId = $config['cache']) { + if (!$container->has('app.cache')) { + throw new \LogicException('You need to specify a service for cache.'); + } + $cacheServiceId = 'app.cache'; + } + $plugins[] = $providerServiceId.'.cache'; + $container->register($providerServiceId.'.cache', CachePlugin::class) + ->setPublic(false) + ->setArguments([new Reference($cacheServiceId), $cacheLifetime, $config['cache_precision']]); + } + + if (isset($config['limit'])) { + $plugins[] = $providerServiceId.'.limit'; + $container->register($providerServiceId.'.limit', LimitPlugin::class) + ->setPublic(false) + ->setArguments([(int) $config['limit']]); + } + + if (isset($config['locale'])) { + $plugins[] = $providerServiceId.'.locale'; + $container->register($providerServiceId.'.locale', LocalePlugin::class) + ->setPublic(false) + ->setArguments([$config['locale']]); + } + + if (isset($config['logger'])) { + $plugins[] = $providerServiceId.'.logger'; + $container->register($providerServiceId.'.logger', LoggerPlugin::class) + ->setPublic(false) + ->setArguments([new Reference($config['logger'])]); + } + + if ($container->has(GeocoderDataCollector::class)) { + $plugins[] = $providerServiceId.'.profiler'; + $container->register($providerServiceId.'.profiler', ProfilingPlugin::class) + ->setPublic(false) + ->setArguments([substr($providerServiceId, strlen('bazinga_geocoder.provider.'))]) + ->addTag('bazinga_geocoder.profiling_plugin'); + } + + return array_map(static fn (string $id) => new Reference($id), $plugins); + } + + /** + * @param array $config + */ + public function getConfiguration(array $config, ContainerBuilder $container): Configuration + { + /** @var bool $debug */ + $debug = $container->getParameter('kernel.debug'); + + return new Configuration($debug); + } + + /** + * @param array $options + * + * @return array + */ + private function findReferences(array $options): array + { + foreach ($options as $key => $value) { + if (is_array($value)) { + $options[$key] = $this->findReferences($value); + } elseif ('_service' === substr((string) $key, -8) || 0 === strpos((string) $value, '@') || 'service' === $key) { + $options[$key] = new Reference(ltrim($value, '@')); + } + } + + return $options; + } + + /** + * @param mixed $factoryClass + */ + private function implementsProviderFactory($factoryClass): bool + { + if (false === $interfaces = class_implements($factoryClass)) { + return false; + } + + return in_array(ProviderFactoryInterface::class, $interfaces, true); + } +} diff --git a/src/DependencyInjection/Compiler/AddProvidersPass.php b/src/DependencyInjection/Compiler/AddProvidersPass.php new file mode 100644 index 00000000..526a94c1 --- /dev/null +++ b/src/DependencyInjection/Compiler/AddProvidersPass.php @@ -0,0 +1,45 @@ + + */ +class AddProvidersPass implements CompilerPassInterface +{ + /** + * Get all providers based on their tag (`bazinga_geocoder.provider`) and + * register them. + * + * @return void + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition(ProviderAggregator::class)) { + return; + } + + $providers = []; + foreach ($container->findTaggedServiceIds('bazinga_geocoder.provider') as $providerId => $attributes) { + $providers[] = new Reference($providerId); + } + + $geocoderDefinition = $container->getDefinition(ProviderAggregator::class); + $geocoderDefinition->addMethodCall('registerProviders', [$providers]); + } +} diff --git a/src/DependencyInjection/Compiler/FactoryValidatorPass.php b/src/DependencyInjection/Compiler/FactoryValidatorPass.php new file mode 100644 index 00000000..6f54fba5 --- /dev/null +++ b/src/DependencyInjection/Compiler/FactoryValidatorPass.php @@ -0,0 +1,52 @@ + + */ +class FactoryValidatorPass implements CompilerPassInterface +{ + /** + * @var string[] + */ + private static $factoryServiceIds = []; + + /** + * @return void + */ + public function process(ContainerBuilder $container) + { + foreach (self::$factoryServiceIds as $id) { + if (!$container->hasAlias($id) && !$container->hasDefinition($id)) { + throw new ServiceNotFoundException(sprintf('Factory with ID "%s" could not be found', $id)); + } + } + } + + /** + * @param string $factoryServiceId + * + * @return void + */ + public static function addFactoryServiceId($factoryServiceId) + { + self::$factoryServiceIds[] = $factoryServiceId; + } +} diff --git a/src/DependencyInjection/Compiler/ProfilerPass.php b/src/DependencyInjection/Compiler/ProfilerPass.php new file mode 100644 index 00000000..3ba985db --- /dev/null +++ b/src/DependencyInjection/Compiler/ProfilerPass.php @@ -0,0 +1,42 @@ + + */ +class ProfilerPass implements CompilerPassInterface +{ + /** + * @return void + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition(GeocoderDataCollector::class)) { + return; + } + + $dataCollector = $container->getDefinition(GeocoderDataCollector::class); + + foreach ($container->findTaggedServiceIds('bazinga_geocoder.profiling_plugin') as $providerId => $attributes) { + $dataCollector->addMethodCall('addInstance', [new Reference($providerId)]); + } + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php new file mode 100644 index 00000000..ca4d3da1 --- /dev/null +++ b/src/DependencyInjection/Configuration.php @@ -0,0 +1,164 @@ + + */ +class Configuration implements ConfigurationInterface +{ + private bool $debug; + + public function __construct(bool $debug) + { + $this->debug = $debug; + } + + /** + * Generates the configuration tree builder. + */ + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('bazinga_geocoder'); + $rootNode = $treeBuilder->getRootNode(); + assert($rootNode instanceof ArrayNodeDefinition); + + $rootNode + ->children() + ->append($this->getProvidersNode()) + ->arrayNode('profiling') + ->addDefaultsIfNotSet() + ->treatFalseLike(['enabled' => false]) + ->treatTrueLike(['enabled' => true]) + ->treatNullLike(['enabled' => $this->debug]) + ->info('Extend the debug profiler with information about requests.') + ->children() + ->booleanNode('enabled') + ->info('Turn the toolbar on or off. Defaults to kernel debug mode.') + ->defaultValue($this->debug) + ->end() + ->end() + ->end() + ->arrayNode('fake_ip') + ->beforeNormalization() + ->ifString() + ->then(function ($value) { + return ['ip' => $value]; + }) + ->end() + ->canBeEnabled() + ->children() + ->scalarNode('local_ip') + ->defaultValue('127.0.0.1') + ->end() + ->scalarNode('ip')->defaultNull()->end() + ->booleanNode('use_faker')->defaultFalse()->end() + ->end() + ->end(); + + return $treeBuilder; + } + + /** + * @return ArrayNodeDefinition + */ + private function getProvidersNode() + { + $treeBuilder = new TreeBuilder('providers'); + $rootNode = $treeBuilder->getRootNode(); + assert($rootNode instanceof ArrayNodeDefinition); + + $rootNode + ->requiresAtLeastOneElement() + ->useAttributeAsKey('name') + ->arrayPrototype() + ->fixXmlConfig('plugin') + ->children() + ->scalarNode('factory')->isRequired()->cannotBeEmpty()->end() + ->variableNode('options')->defaultValue([])->end() + ->scalarNode('cache')->defaultNull()->end() + ->scalarNode('cache_lifetime')->defaultNull()->end() + ->scalarNode('cache_precision') + ->defaultNull() + ->info('Precision of the coordinates to cache.') + ->end() + ->scalarNode('limit')->defaultNull()->end() + ->scalarNode('locale')->defaultNull()->end() + ->scalarNode('logger')->defaultNull()->end() + ->arrayNode('aliases') + ->scalarPrototype()->end() + ->end() + ->append($this->createClientPluginNode()) + ->end() + ->end(); + + return $rootNode; + } + + /** + * Create plugin node of a client. + * + * @return ArrayNodeDefinition The plugin node + */ + private function createClientPluginNode() + { + $treeBuilder = new TreeBuilder('plugins'); + $rootNode = $treeBuilder->getRootNode(); + assert($rootNode instanceof ArrayNodeDefinition); + + /** @var ArrayNodeDefinition $pluginList */ + $pluginList = $rootNode + ->info('A list of plugin service ids. The order is important.') + ->arrayPrototype() + ; + $pluginList + // support having just a service id in the list + ->beforeNormalization() + ->always(function ($plugin) { + if (is_string($plugin)) { + return [ + 'reference' => [ + 'enabled' => true, + 'id' => $plugin, + ], + ]; + } + + return $plugin; + }) + ->end() + ; + + $pluginList + ->children() + ->arrayNode('reference') + ->canBeEnabled() + ->info('Reference to a plugin service') + ->children() + ->scalarNode('id') + ->info('Service id of a plugin') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->end() + ->end() + ->end() + ->end(); + + return $rootNode; + } +} diff --git a/src/Doctrine/ORM/GeocoderListener.php b/src/Doctrine/ORM/GeocoderListener.php new file mode 100644 index 00000000..070d3a82 --- /dev/null +++ b/src/Doctrine/ORM/GeocoderListener.php @@ -0,0 +1,132 @@ + + */ +class GeocoderListener implements EventSubscriber +{ + private DriverInterface $driver; + private Provider $geocoder; + + public function __construct(Provider $geocoder, DriverInterface $driver) + { + $this->driver = $driver; + $this->geocoder = $geocoder; + } + + /** + * @return list + */ + public function getSubscribedEvents(): array + { + return [ + Events::onFlush, + ]; + } + + /** + * @return void + */ + public function onFlush(OnFlushEventArgs $args) + { + $em = method_exists($args, 'getObjectManager') ? $args->getObjectManager() : $args->getEntityManager(); + $uow = $em->getUnitOfWork(); + + foreach ($uow->getScheduledEntityInsertions() as $entity) { + if (!$this->driver->isGeocodeable($entity)) { + continue; + } + + /** @var ClassMetadata $metadata */ + $metadata = $this->driver->loadMetadataFromObject($entity); + + $this->geocodeEntity($metadata, $entity); + + $uow->recomputeSingleEntityChangeSet( + $em->getClassMetadata(get_class($entity)), + $entity + ); + } + + foreach ($uow->getScheduledEntityUpdates() as $entity) { + if (!$this->driver->isGeocodeable($entity)) { + continue; + } + + /** @var ClassMetadata $metadata */ + $metadata = $this->driver->loadMetadataFromObject($entity); + + if (!$this->shouldGeocode($metadata, $uow, $entity)) { + continue; + } + + $this->geocodeEntity($metadata, $entity); + + $uow->recomputeSingleEntityChangeSet( + $em->getClassMetadata(get_class($entity)), + $entity + ); + } + } + + /** + * @param object $entity + * + * @return void + */ + private function geocodeEntity(ClassMetadata $metadata, $entity) + { + if (null !== $metadata->addressGetter) { + $address = $metadata->addressGetter->invoke($entity); + } else { + $address = $metadata->addressProperty->getValue($entity); + } + + if (empty($address) || !is_string($address)) { + return; + } + + $results = $this->geocoder->geocodeQuery(GeocodeQuery::create($address)); + + if (!$results->isEmpty()) { + $result = $results->first(); + $metadata->latitudeProperty->setValue($entity, $result->getCoordinates()->getLatitude()); + $metadata->longitudeProperty->setValue($entity, $result->getCoordinates()->getLongitude()); + } + } + + /** + * @param object $entity + */ + private function shouldGeocode(ClassMetadata $metadata, UnitOfWork $unitOfWork, $entity): bool + { + if (null !== $metadata->addressGetter) { + return true; + } + + $changeSet = $unitOfWork->getEntityChangeSet($entity); + + return isset($changeSet[$metadata->addressProperty->getName()]); + } +} diff --git a/src/Mapping/Annotations/Address.php b/src/Mapping/Annotations/Address.php new file mode 100644 index 00000000..3dde2ecc --- /dev/null +++ b/src/Mapping/Annotations/Address.php @@ -0,0 +1,23 @@ + + * + * @Annotation + */ +class Address +{ +} diff --git a/src/Mapping/Annotations/Geocodeable.php b/src/Mapping/Annotations/Geocodeable.php new file mode 100644 index 00000000..fce6311e --- /dev/null +++ b/src/Mapping/Annotations/Geocodeable.php @@ -0,0 +1,23 @@ + + * + * @Annotation + */ +class Geocodeable +{ +} diff --git a/src/Mapping/Annotations/Latitude.php b/src/Mapping/Annotations/Latitude.php new file mode 100644 index 00000000..356a5778 --- /dev/null +++ b/src/Mapping/Annotations/Latitude.php @@ -0,0 +1,23 @@ + + * + * @Annotation + */ +class Latitude +{ +} diff --git a/src/Mapping/Annotations/Longitude.php b/src/Mapping/Annotations/Longitude.php new file mode 100644 index 00000000..71dddaef --- /dev/null +++ b/src/Mapping/Annotations/Longitude.php @@ -0,0 +1,23 @@ + + * + * @Annotation + */ +class Longitude +{ +} diff --git a/src/Mapping/ClassMetadata.php b/src/Mapping/ClassMetadata.php new file mode 100644 index 00000000..891bffb2 --- /dev/null +++ b/src/Mapping/ClassMetadata.php @@ -0,0 +1,39 @@ + + */ +class ClassMetadata +{ + /** + * @var \ReflectionProperty + */ + public $addressProperty; + + /** + * @var \ReflectionProperty + */ + public $latitudeProperty; + + /** + * @var \ReflectionProperty + */ + public $longitudeProperty; + + /** + * @var \ReflectionMethod + */ + public $addressGetter; +} diff --git a/src/Mapping/Driver/AnnotationDriver.php b/src/Mapping/Driver/AnnotationDriver.php new file mode 100644 index 00000000..71089902 --- /dev/null +++ b/src/Mapping/Driver/AnnotationDriver.php @@ -0,0 +1,87 @@ + + */ +class AnnotationDriver implements DriverInterface +{ + private Reader $reader; + + public function __construct(Reader $reader) + { + $this->reader = $reader; + } + + public function isGeocodeable($object): bool + { + $reflection = self::getReflection($object); + + return (bool) $this->reader->getClassAnnotation($reflection, Annotations\Geocodeable::class); + } + + public function loadMetadataFromObject($object) + { + $reflection = self::getReflection($object); + + if (!$annotation = $this->reader->getClassAnnotation($reflection, Annotations\Geocodeable::class)) { + throw new Exception\MappingException(sprintf('The class %s is not geocodeable', get_class($object))); + } + + $metadata = new ClassMetadata(); + + foreach ($reflection->getProperties() as $property) { + foreach ($this->reader->getPropertyAnnotations($property) as $annotation) { + if ($annotation instanceof Annotations\Latitude) { + $property->setAccessible(true); + $metadata->latitudeProperty = $property; + } elseif ($annotation instanceof Annotations\Longitude) { + $property->setAccessible(true); + $metadata->longitudeProperty = $property; + } elseif ($annotation instanceof Annotations\Address) { + $property->setAccessible(true); + $metadata->addressProperty = $property; + } + } + } + + foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if ($this->reader->getMethodAnnotation($method, Annotations\Address::class)) { + if (0 !== $method->getNumberOfRequiredParameters()) { + throw new \Exception('You can not use a method requiring parameters with @Address annotation!'); + } + + $metadata->addressGetter = $method; + } + } + + return $metadata; + } + + private static function getReflection(object $object): \ReflectionClass + { + if (class_exists(ClassUtils::class)) { + return ClassUtils::newReflectionObject($object); + } + + return new \ReflectionClass(DefaultProxyClassNameResolver::getClass($object)); + } +} diff --git a/src/Mapping/Driver/AttributeDriver.php b/src/Mapping/Driver/AttributeDriver.php new file mode 100644 index 00000000..703b0efb --- /dev/null +++ b/src/Mapping/Driver/AttributeDriver.php @@ -0,0 +1,92 @@ + + */ +final class AttributeDriver implements DriverInterface +{ + public function isGeocodeable($object): bool + { + if (PHP_VERSION_ID < 80000) { + return false; + } + + $reflection = self::getReflection($object); + + return count($reflection->getAttributes(Annotations\Geocodeable::class)) > 0; + } + + /** + * @throws MappingException + */ + public function loadMetadataFromObject($object): ClassMetadata + { + if (PHP_VERSION_ID < 80000) { + throw new MappingException(sprintf('The class %s is not geocodeable', get_class($object))); + } + + $reflection = self::getReflection($object); + + $attributes = $reflection->getAttributes(Annotations\Geocodeable::class); + + if (0 === count($attributes)) { + throw new MappingException(sprintf('The class %s is not geocodeable', get_class($object))); + } + + $metadata = new ClassMetadata(); + + foreach ($reflection->getProperties() as $property) { + foreach ($property->getAttributes() as $attribute) { + if (Annotations\Latitude::class === $attribute->getName()) { + $property->setAccessible(true); + $metadata->latitudeProperty = $property; + } elseif (Annotations\Longitude::class === $attribute->getName()) { + $property->setAccessible(true); + $metadata->longitudeProperty = $property; + } elseif (Annotations\Address::class === $attribute->getName()) { + $property->setAccessible(true); + $metadata->addressProperty = $property; + } + } + } + + foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if (count($method->getAttributes(Annotations\Address::class)) > 0) { + if (0 !== $method->getNumberOfRequiredParameters()) { + throw new MappingException('You can not use a method requiring parameters with #[Address] attribute!'); + } + + $metadata->addressGetter = $method; + } + } + + return $metadata; + } + + private static function getReflection(object $object): \ReflectionClass + { + if (class_exists(ClassUtils::class)) { + return ClassUtils::newReflectionObject($object); + } + + return new \ReflectionClass(DefaultProxyClassNameResolver::getClass($object)); + } +} diff --git a/src/Mapping/Driver/DriverInterface.php b/src/Mapping/Driver/DriverInterface.php new file mode 100644 index 00000000..0f38cfcc --- /dev/null +++ b/src/Mapping/Driver/DriverInterface.php @@ -0,0 +1,30 @@ + + */ +class MappingException extends \Exception +{ +} diff --git a/src/Plugin/FakeIpPlugin.php b/src/Plugin/FakeIpPlugin.php new file mode 100644 index 00000000..e45ff2d1 --- /dev/null +++ b/src/Plugin/FakeIpPlugin.php @@ -0,0 +1,71 @@ + + */ +class FakeIpPlugin implements Plugin +{ + private ?string $needle; + private ?string $replacement; + private ?Generator $faker = null; + + public function __construct(?string $needle, ?string $replacement = null, bool $useFaker = false) + { + $this->needle = $needle; + $this->replacement = $replacement; + + if ($useFaker) { + $this->faker = new Generator(); + $this->faker->addProvider(new Internet($this->faker)); + } + } + + /** + * @return Promise + */ + public function handleQuery(Query $query, callable $next, callable $first) + { + if (!$query instanceof GeocodeQuery) { + return $next($query); + } + + $replacement = $this->replacement; + + if (null !== $this->faker) { + $replacement = $this->faker->ipv4(); + } + + if (null !== $this->needle && '' !== $this->needle) { + $text = str_replace($this->needle, $replacement, $query->getText(), $count); + + if ($count > 0) { + $query = $query->withText($text); + } + } else { + $query = $query->withText($replacement); + } + + return $next($query); + } +} diff --git a/src/Plugin/ProfilingPlugin.php b/src/Plugin/ProfilingPlugin.php new file mode 100644 index 00000000..c2290779 --- /dev/null +++ b/src/Plugin/ProfilingPlugin.php @@ -0,0 +1,101 @@ + + */ +class ProfilingPlugin implements Plugin +{ + /** + * @var list + */ + private $queries = []; + + /** + * @var string service id of the provider + */ + private $name; + + public function __construct(string $name) + { + $this->name = $name; + } + + /** + * @return Promise + */ + public function handleQuery(Query $query, callable $next, callable $first) + { + $startTime = microtime(true); + + return $next($query)->then(function (Collection $result) use ($query, $startTime) { + $duration = (microtime(true) - $startTime) * 1000; + $this->logQuery($query, $duration, $result); + + return $result; + }, function (Exception $exception) use ($query, $startTime) { + $duration = (microtime(true) - $startTime) * 1000; + $this->logQuery($query, $duration, $exception); + + throw $exception; + }); + } + + /** + * @param mixed $result + * + * @return void + */ + private function logQuery(Query $query, float $duration, $result = null) + { + if ($query instanceof GeocodeQuery) { + $queryString = $query->getText(); + } elseif ($query instanceof ReverseQuery) { + $queryString = sprintf('(%s, %s)', $query->getCoordinates()->getLongitude(), $query->getCoordinates()->getLatitude()); + } else { + throw new LogicException('First parameter to ProfilingProvider::logQuery must be a Query'); + } + + $this->queries[] = [ + 'query' => $query, + 'queryString' => $queryString, + 'duration' => $duration, + 'providerName' => $this->getName(), + 'result' => $result, + 'resultCount' => $result instanceof Collection ? $result->count() : 0, + ]; + } + + /** + * @return list + */ + public function getQueries(): array + { + return $this->queries; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/src/ProviderFactory/AbstractFactory.php b/src/ProviderFactory/AbstractFactory.php new file mode 100644 index 00000000..83da203e --- /dev/null +++ b/src/ProviderFactory/AbstractFactory.php @@ -0,0 +1,102 @@ + + */ +abstract class AbstractFactory implements ProviderFactoryInterface +{ + /** + * @var list + */ + protected static $dependencies = []; + + protected ?ClientInterface $httpClient; + + public function __construct(?ClientInterface $httpClient = null) + { + $this->httpClient = $httpClient; + } + + /** + * @param array $config + */ + abstract protected function getProvider(array $config): Provider; + + public function createProvider(array $options = []): Provider + { + $this->verifyDependencies(); + + $resolver = new OptionsResolver(); + static::configureOptionResolver($resolver); + $config = $resolver->resolve($options); + + return $this->getProvider($config); + } + + public static function validate(array $options, $providerName) + { + static::verifyDependencies(); + + $resolver = new OptionsResolver(); + static::configureOptionResolver($resolver); + + try { + $resolver->resolve($options); + } catch (\Exception $e) { + $message = sprintf( + 'Error while configure provider "%s". Verify your configuration at "bazinga_geocoder.providers.%s.options". %s', + $providerName, + $providerName, + $e->getMessage() + ); + + throw new InvalidConfigurationException($message, $e->getCode(), $e); + } + } + + /** + * Make sure that we have the required class and throw and exception if we don't. + * + * @return void + * + * @throws \LogicException + */ + protected static function verifyDependencies() + { + foreach (static::$dependencies as $dependency) { + if (!class_exists($dependency['requiredClass'])) { + throw new \LogicException(sprintf('You must install the "%s" package to use the "%s" factory.', $dependency['packageName'], static::class)); + } + } + } + + /** + * By default, we do not have any options to configure. A factory should override this function and configure + * the options resolver. + * + * @return void + */ + protected static function configureOptionResolver(OptionsResolver $resolver) + { + } +} diff --git a/src/ProviderFactory/AlgoliaFactory.php b/src/ProviderFactory/AlgoliaFactory.php new file mode 100644 index 00000000..7bb28375 --- /dev/null +++ b/src/ProviderFactory/AlgoliaFactory.php @@ -0,0 +1,53 @@ + AlgoliaPlaces::class, 'packageName' => 'geocoder-php/algolia-places-provider'], + ]; + + /** + * @param array{api_key: ?string, app_id: ?string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new AlgoliaPlaces($httpClient, $config['api_key'], $config['app_id']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + 'api_key' => null, + 'app_id' => null, + ]); + + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('api_key', ['string', 'null']); + $resolver->setAllowedTypes('app_id', ['string', 'null']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/ArcGISOnlineFactory.php b/src/ProviderFactory/ArcGISOnlineFactory.php new file mode 100644 index 00000000..e35c1f36 --- /dev/null +++ b/src/ProviderFactory/ArcGISOnlineFactory.php @@ -0,0 +1,51 @@ + ArcGISOnline::class, 'packageName' => 'geocoder-php/arcgis-online-provider'], + ]; + + /** + * @param array{source_country: ?string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new ArcGISOnline($httpClient, $config['source_country']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + 'source_country' => null, + ]); + + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('source_country', ['string', 'null']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/BingMapsFactory.php b/src/ProviderFactory/BingMapsFactory.php new file mode 100644 index 00000000..03abecf9 --- /dev/null +++ b/src/ProviderFactory/BingMapsFactory.php @@ -0,0 +1,51 @@ + BingMaps::class, 'packageName' => 'geocoder-php/bing-maps-provider'], + ]; + + /** + * @param array{api_key: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new BingMaps($httpClient, $config['api_key']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + ]); + + $resolver->setRequired('api_key'); + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('api_key', ['string']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/ChainFactory.php b/src/ProviderFactory/ChainFactory.php new file mode 100644 index 00000000..77eb4404 --- /dev/null +++ b/src/ProviderFactory/ChainFactory.php @@ -0,0 +1,52 @@ + + */ +final class ChainFactory extends AbstractFactory implements LoggerAwareInterface +{ + use LoggerAwareTrait; + + protected static $dependencies = [ + ['requiredClass' => Chain::class, 'packageName' => 'geocoder-php/chain-provider'], + ]; + + /** + * @param array{services: Provider[]} $config + */ + protected function getProvider(array $config): Provider + { + $provider = new Chain($config['services']); + if (null !== $this->logger) { + $provider->setLogger($this->logger); + } + + return $provider; + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + parent::configureOptionResolver($resolver); + + $resolver->setRequired('services'); + $resolver->setAllowedTypes('services', ['array']); + } +} diff --git a/src/ProviderFactory/FreeGeoIpFactory.php b/src/ProviderFactory/FreeGeoIpFactory.php new file mode 100644 index 00000000..53da263d --- /dev/null +++ b/src/ProviderFactory/FreeGeoIpFactory.php @@ -0,0 +1,51 @@ + FreeGeoIp::class, 'packageName' => 'geocoder-php/free-geoip-provider'], + ]; + + /** + * @param array{base_url: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new FreeGeoIp($httpClient, $config['base_url']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + 'base_url' => 'https://freegeoip.app/json/%s', + ]); + + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('base_url', ['string']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/GeoIP2Factory.php b/src/ProviderFactory/GeoIP2Factory.php new file mode 100644 index 00000000..77dfdbce --- /dev/null +++ b/src/ProviderFactory/GeoIP2Factory.php @@ -0,0 +1,75 @@ + GeoIP2::class, 'packageName' => 'geocoder-php/geoip2-provider'], + ]; + + /** + * @param array{provider: string, provider_service: ?ProviderInterface, model: string, user_id: string|int|null, license_key: string|null, locales: list, webservice_options: array, database_filename: ?string} $config + */ + protected function getProvider(array $config): Provider + { + $provider = $config['provider']; + if ('webservice' === $provider) { + $userId = isset($config['user_id']) ? (int) $config['user_id'] : null; + + $provider = new Client($userId, $config['license_key'], $config['locales'], $config['webservice_options']); + } elseif ('database' === $provider) { + $provider = new Reader($config['database_filename'], $config['locales']); + } else { + $provider = $config['provider_service']; + } + + $adapter = new GeoIP2Adapter($provider, $config['model']); + + return new GeoIP2($adapter); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'model' => GeoIP2Adapter::GEOIP2_MODEL_CITY, + 'database_filename' => null, + 'user_id' => null, + 'license_key' => null, + 'webservice_options' => [], + 'locales' => ['en'], + 'provider_service' => null, + ]); + + $resolver->setRequired('provider'); + $resolver->setAllowedTypes('provider', ['string']); + $resolver->setAllowedTypes('provider_service', [ProviderInterface::class, 'null']); + $resolver->setAllowedTypes('model', ['string']); + $resolver->setAllowedTypes('user_id', ['string', 'int', 'null']); + $resolver->setAllowedTypes('license_key', ['string', 'null']); + $resolver->setAllowedTypes('locales', ['array']); + $resolver->setAllowedTypes('webservice_options', ['array']); + $resolver->setAllowedTypes('database_filename', ['string', 'null']); + + $resolver->setAllowedValues('model', [GeoIP2Adapter::GEOIP2_MODEL_CITY, GeoIP2Adapter::GEOIP2_MODEL_COUNTRY]); + $resolver->setAllowedValues('provider', ['webservice', 'database', 'service']); + } +} diff --git a/src/ProviderFactory/GeoIPsFactory.php b/src/ProviderFactory/GeoIPsFactory.php new file mode 100644 index 00000000..b5a6f98b --- /dev/null +++ b/src/ProviderFactory/GeoIPsFactory.php @@ -0,0 +1,53 @@ + GeoIPs::class, 'packageName' => 'geocoder-php/geoips-provider'], + ]; + + /** + * @param array{api_key: string, httplug_client: ?HttpClient} $config + */ + protected function getProvider(array $config): Provider + { + @trigger_error('Bazinga\GeocoderBundle\ProviderFactory\GeoIPsFactory is deprecated since 5.6, to be removed in 6.0. See https://github.com/geocoder-php/Geocoder/issues/965', E_USER_DEPRECATED); + + $httplug = $config['httplug_client'] ?: $this->httpClient ?? HttpClientDiscovery::find(); + assert($httplug instanceof HttpClient); + + return new GeoIPs($httplug, $config['api_key']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + ]); + + $resolver->setRequired('api_key'); + $resolver->setAllowedTypes('httplug_client', [HttpClient::class, 'null']); + $resolver->setAllowedTypes('api_key', ['string']); + } +} diff --git a/src/ProviderFactory/GeoPluginFactory.php b/src/ProviderFactory/GeoPluginFactory.php new file mode 100644 index 00000000..2162676b --- /dev/null +++ b/src/ProviderFactory/GeoPluginFactory.php @@ -0,0 +1,49 @@ + GeoPlugin::class, 'packageName' => 'geocoder-php/geo-plugin-provider'], + ]; + + /** + * @param array{http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new GeoPlugin($httpClient); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + ]); + + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/GeoipFactory.php b/src/ProviderFactory/GeoipFactory.php new file mode 100644 index 00000000..03a874a7 --- /dev/null +++ b/src/ProviderFactory/GeoipFactory.php @@ -0,0 +1,36 @@ + Geoip::class, 'packageName' => 'geocoder-php/geoip-provider'], + ]; + + /** + * @param array{} $config + */ + protected function getProvider(array $config): Provider + { + return new Geoip(); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + } +} diff --git a/src/ProviderFactory/GeonamesFactory.php b/src/ProviderFactory/GeonamesFactory.php new file mode 100644 index 00000000..81c1afbc --- /dev/null +++ b/src/ProviderFactory/GeonamesFactory.php @@ -0,0 +1,51 @@ + Geonames::class, 'packageName' => 'geocoder-php/geonames-provider'], + ]; + + /** + * @param array{username: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new Geonames($httpClient, $config['username']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + ]); + + $resolver->setRequired('username'); + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('username', ['string']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/GoogleMapsFactory.php b/src/ProviderFactory/GoogleMapsFactory.php new file mode 100644 index 00000000..2f548eaf --- /dev/null +++ b/src/ProviderFactory/GoogleMapsFactory.php @@ -0,0 +1,53 @@ + GoogleMaps::class, 'packageName' => 'geocoder-php/google-maps-provider'], + ]; + + /** + * @param array{api_key: ?string, region: ?string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new GoogleMaps($httpClient, $config['region'], $config['api_key']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + 'region' => null, + 'api_key' => null, + ]); + + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('region', ['string', 'null']); + $resolver->setAllowedTypes('api_key', ['string', 'null']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "%name%" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/GoogleMapsPlacesFactory.php b/src/ProviderFactory/GoogleMapsPlacesFactory.php new file mode 100644 index 00000000..82ac985b --- /dev/null +++ b/src/ProviderFactory/GoogleMapsPlacesFactory.php @@ -0,0 +1,51 @@ + GoogleMapsPlaces::class, 'packageName' => 'geocoder-php/google-maps-places-provider'], + ]; + + /** + * @param array{api_key: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new GoogleMapsPlaces($httpClient, $config['api_key']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + ]); + + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setRequired(['api_key']); + $resolver->setAllowedTypes('api_key', ['string']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/HereFactory.php b/src/ProviderFactory/HereFactory.php new file mode 100644 index 00000000..48309fba --- /dev/null +++ b/src/ProviderFactory/HereFactory.php @@ -0,0 +1,69 @@ + Here::class, 'packageName' => 'geocoder-php/here-provider'], + ]; + + /** + * @param array{app_key: ?string, app_id: ?string, app_code: ?string, use_cit: bool, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + if (empty($config['app_key']) && empty($config['app_id']) && empty($config['app_code'])) { + throw new \InvalidArgumentException('No authentication key provided. Here requires app_key or app_code and app_id.'); + } + + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + if (!empty($config['app_key'])) { + if (!method_exists(Here::class, 'createUsingApiKey')) { + throw new \InvalidArgumentException('Here provider has no support for `creatingUsingApiKey` method.'); + } + + return Here::createUsingApiKey($httpClient, $config['app_key'], $config['use_cit']); + } + + return new Here($httpClient, $config['app_id'], $config['app_code'], $config['use_cit']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + 'use_cit' => false, + 'app_key' => null, + 'app_id' => null, + 'app_code' => null, + ]); + + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('app_key', ['string', 'null']); + $resolver->setAllowedTypes('app_id', ['string', 'null']); + $resolver->setAllowedTypes('app_code', ['string', 'null']); + $resolver->setAllowedTypes('use_cit', ['bool', 'false']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/HostIpFactory.php b/src/ProviderFactory/HostIpFactory.php new file mode 100644 index 00000000..864e0229 --- /dev/null +++ b/src/ProviderFactory/HostIpFactory.php @@ -0,0 +1,49 @@ + HostIp::class, 'packageName' => 'geocoder-php/host-ip-provider'], + ]; + + /** + * @param array{http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new HostIp($httpClient); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + ]); + + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/IpInfoDbFactory.php b/src/ProviderFactory/IpInfoDbFactory.php new file mode 100644 index 00000000..d065f8d8 --- /dev/null +++ b/src/ProviderFactory/IpInfoDbFactory.php @@ -0,0 +1,53 @@ + IpInfoDb::class, 'packageName' => 'geocoder-php/ip-info-db-provider'], + ]; + + /** + * @param array{api_key: string, precision: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new IpInfoDb($httpClient, $config['api_key'], $config['precision']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + 'precision' => 'city', + ]); + + $resolver->setRequired('api_key'); + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('api_key', ['string']); + $resolver->setAllowedTypes('precision', ['string']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/IpInfoFactory.php b/src/ProviderFactory/IpInfoFactory.php new file mode 100644 index 00000000..2420f79c --- /dev/null +++ b/src/ProviderFactory/IpInfoFactory.php @@ -0,0 +1,49 @@ + IpInfo::class, 'packageName' => 'geocoder-php/ip-info-provider'], + ]; + + /** + * @param array{http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new IpInfo($httpClient); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + ]); + + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/IpstackFactory.php b/src/ProviderFactory/IpstackFactory.php new file mode 100644 index 00000000..5396c90b --- /dev/null +++ b/src/ProviderFactory/IpstackFactory.php @@ -0,0 +1,51 @@ + Ipstack::class, 'packageName' => 'geocoder-php/ipstack-provider'], + ]; + + /** + * @param array{api_key: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new Ipstack($httpClient, $config['api_key']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + ]); + + $resolver->setRequired('api_key'); + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('api_key', ['string']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/LocationIQFactory.php b/src/ProviderFactory/LocationIQFactory.php new file mode 100644 index 00000000..ce793d3b --- /dev/null +++ b/src/ProviderFactory/LocationIQFactory.php @@ -0,0 +1,49 @@ + LocationIQ::class, 'packageName' => 'geocoder-php/locationiq-provider'], + ]; + + /** + * @param array{api_key: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new LocationIQ($httpClient, $config['api_key']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + ]); + + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setRequired(['api_key']); + $resolver->setAllowedTypes('api_key', ['string']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/MapQuestFactory.php b/src/ProviderFactory/MapQuestFactory.php new file mode 100644 index 00000000..6207e28b --- /dev/null +++ b/src/ProviderFactory/MapQuestFactory.php @@ -0,0 +1,53 @@ + MapQuest::class, 'packageName' => 'geocoder-php/mapquest-provider'], + ]; + + /** + * @param array{api_key: string, licensed: bool, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new MapQuest($httpClient, $config['api_key'], $config['licensed']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + 'licensed' => false, + ]); + + $resolver->setRequired('api_key'); + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('api_key', ['string']); + $resolver->setAllowedTypes('licensed', ['boolean']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/MapboxFactory.php b/src/ProviderFactory/MapboxFactory.php new file mode 100644 index 00000000..26229998 --- /dev/null +++ b/src/ProviderFactory/MapboxFactory.php @@ -0,0 +1,53 @@ + Mapbox::class, 'packageName' => 'geocoder-php/mapbox-provider'], + ]; + + /** + * @param array{api_key: string, country: ?string, mode: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new Mapbox($httpClient, $config['api_key'], $config['country'], $config['mode']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + 'country' => null, + 'mode' => Mapbox::GEOCODING_MODE_PLACES, + ]); + + $resolver->setRequired('api_key'); + $resolver->setAllowedTypes('api_key', ['string']); + $resolver->setAllowedTypes('mode', ['string']); + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('country', ['string', 'null']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/MapzenFactory.php b/src/ProviderFactory/MapzenFactory.php new file mode 100644 index 00000000..ed74db6e --- /dev/null +++ b/src/ProviderFactory/MapzenFactory.php @@ -0,0 +1,53 @@ + Mapzen::class, 'packageName' => 'geocoder-php/mapzen-provider'], + ]; + + /** + * @param array{api_key: string, httplug_client: ?HttpClient} $config + */ + protected function getProvider(array $config): Provider + { + @trigger_error('Bazinga\GeocoderBundle\ProviderFactory\MapzenFactory is deprecated since 5.6, to be removed in 6.0. See https://github.com/geocoder-php/Geocoder/issues/808', E_USER_DEPRECATED); + + $httplug = $config['httplug_client'] ?: $this->httpClient ?? HttpClientDiscovery::find(); + assert($httplug instanceof HttpClient); + + return new Mapzen($httplug, $config['api_key']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + ]); + + $resolver->setRequired('api_key'); + $resolver->setAllowedTypes('httplug_client', [HttpClient::class, 'null']); + $resolver->setAllowedTypes('api_key', ['string']); + } +} diff --git a/src/ProviderFactory/MaxMindBinaryFactory.php b/src/ProviderFactory/MaxMindBinaryFactory.php new file mode 100644 index 00000000..009792f7 --- /dev/null +++ b/src/ProviderFactory/MaxMindBinaryFactory.php @@ -0,0 +1,43 @@ + MaxMindBinary::class, 'packageName' => 'geocoder-php/maxmind-binary-provider'], + ]; + + /** + * @param array{dat_file: string, open_flag: ?int} $config + */ + protected function getProvider(array $config): Provider + { + return new MaxMindBinary($config['dat_file'], $config['open_flag']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'open_flag' => null, + ]); + + $resolver->setRequired('dat_file'); + $resolver->setAllowedTypes('dat_file', ['string']); + $resolver->setAllowedTypes('open_flag', ['int', 'null']); + } +} diff --git a/src/ProviderFactory/MaxMindFactory.php b/src/ProviderFactory/MaxMindFactory.php new file mode 100644 index 00000000..c551f7b1 --- /dev/null +++ b/src/ProviderFactory/MaxMindFactory.php @@ -0,0 +1,53 @@ + MaxMind::class, 'packageName' => 'geocoder-php/maxmind-provider'], + ]; + + /** + * @param array{api_key: string, endpoint: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new MaxMind($httpClient, $config['api_key'], $config['endpoint']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + 'endpoint' => MaxMind::CITY_EXTENDED_SERVICE, + ]); + + $resolver->setRequired('api_key'); + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('api_key', ['string']); + $resolver->setAllowedValues('endpoint', [MaxMind::CITY_EXTENDED_SERVICE, MaxMind::OMNI_SERVICE]); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/NominatimFactory.php b/src/ProviderFactory/NominatimFactory.php new file mode 100644 index 00000000..28dcd2a1 --- /dev/null +++ b/src/ProviderFactory/NominatimFactory.php @@ -0,0 +1,54 @@ + Nominatim::class, 'packageName' => 'geocoder-php/nominatim-provider'], + ]; + + /** + * @param array{root_url: string, user_agent: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new Nominatim($httpClient, $config['root_url'], $config['user_agent']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + 'root_url' => 'https://nominatim.openstreetmap.org', + 'user_agent' => 'BazingaGeocoderBundle', + ]); + + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('root_url', ['string']); + $resolver->setAllowedTypes('user_agent', ['string']); + $resolver->setRequired('user_agent'); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/OpenCageFactory.php b/src/ProviderFactory/OpenCageFactory.php new file mode 100644 index 00000000..65a9fc37 --- /dev/null +++ b/src/ProviderFactory/OpenCageFactory.php @@ -0,0 +1,51 @@ + OpenCage::class, 'packageName' => 'geocoder-php/open-cage-provider'], + ]; + + /** + * @param array{api_key: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new OpenCage($httpClient, $config['api_key']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + ]); + + $resolver->setRequired('api_key'); + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('api_key', ['string']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/OpenRouteServiceFactory.php b/src/ProviderFactory/OpenRouteServiceFactory.php new file mode 100644 index 00000000..86214fc5 --- /dev/null +++ b/src/ProviderFactory/OpenRouteServiceFactory.php @@ -0,0 +1,52 @@ + OpenRouteService::class, 'packageName' => 'geocoder-php/openrouteservice-provider'], + ]; + + /** + * @param array{api_key: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new OpenRouteService($httpClient, $config['api_key']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + 'api_key' => null, + ]); + + $resolver->setRequired('api_key'); + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('api_key', ['string']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/PickPointFactory.php b/src/ProviderFactory/PickPointFactory.php new file mode 100644 index 00000000..35c95aa2 --- /dev/null +++ b/src/ProviderFactory/PickPointFactory.php @@ -0,0 +1,51 @@ + PickPoint::class, 'packageName' => 'geocoder-php/pickpoint-provider'], + ]; + + /** + * @param array{api_key: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new PickPoint($httpClient, $config['api_key']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + ]); + + $resolver->setRequired('api_key'); + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('api_key', ['string']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/PluginProviderFactory.php b/src/ProviderFactory/PluginProviderFactory.php new file mode 100644 index 00000000..af1a2ce7 --- /dev/null +++ b/src/ProviderFactory/PluginProviderFactory.php @@ -0,0 +1,43 @@ + + */ +final class PluginProviderFactory +{ + /** + * @param Plugin[] $plugins + * @param ProviderFactoryInterface|callable $factory + * @param array $config config to the client factory + * @param array{max_restarts?: int<0, max>} $pluginProviderOptions config forwarded to the PluginProvider + */ + public static function createPluginProvider(array $plugins, $factory, array $config, array $pluginProviderOptions = []): PluginProvider + { + if ($factory instanceof ProviderFactoryInterface) { + $client = $factory->createProvider($config); + } elseif (is_callable($factory)) { + $client = $factory($config); + } else { + throw new \RuntimeException(sprintf('Second argument to PluginProviderFactory::createPluginProvider must be a "%s" or a callable.', ProviderFactoryInterface::class)); + } + + return new PluginProvider($client, $plugins, $pluginProviderOptions); + } +} diff --git a/src/ProviderFactory/ProviderFactoryInterface.php b/src/ProviderFactory/ProviderFactoryInterface.php new file mode 100644 index 00000000..5f96dbaf --- /dev/null +++ b/src/ProviderFactory/ProviderFactoryInterface.php @@ -0,0 +1,44 @@ + + */ +interface ProviderFactoryInterface +{ + /** + * @param array $options + */ + public function createProvider(array $options = []): Provider; + + /** + * Make sure the options are valid and the dependencies are met. + * + * @param array $options the options the user has provided + * @param string $providerName the name the user has chosen for this provider + * + * @return void + * + * @throws \LogicException If the factory has missing dependencies + * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException If an option name is undefined + * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException If an option doesn't fulfill the specified validation rules + * @throws \Symfony\Component\OptionsResolver\Exception\MissingOptionsException If a required option is missing + * @throws \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException If there is a cyclic dependency between lazy options and/or normalizers + * @throws \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException If a lazy option reads an unavailable option + * @throws \Symfony\Component\OptionsResolver\Exception\AccessException If called from a lazy option or normalizer + */ + public static function validate(array $options, $providerName); +} diff --git a/src/ProviderFactory/TomTomFactory.php b/src/ProviderFactory/TomTomFactory.php new file mode 100644 index 00000000..27820e78 --- /dev/null +++ b/src/ProviderFactory/TomTomFactory.php @@ -0,0 +1,51 @@ + TomTom::class, 'packageName' => 'geocoder-php/tomtom-provider'], + ]; + + /** + * @param array{api_key: string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new TomTom($httpClient, $config['api_key']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + ]); + + $resolver->setRequired('api_key'); + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('api_key', ['string']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/ProviderFactory/YandexFactory.php b/src/ProviderFactory/YandexFactory.php new file mode 100644 index 00000000..9792c090 --- /dev/null +++ b/src/ProviderFactory/YandexFactory.php @@ -0,0 +1,53 @@ + Yandex::class, 'packageName' => 'geocoder-php/yandex-provider'], + ]; + + /** + * @param array{toponym: ?string, api_key: ?string, http_client: ?ClientInterface, httplug_client: ?ClientInterface} $config + */ + protected function getProvider(array $config): Provider + { + $httpClient = $config['http_client'] ?? $config['httplug_client'] ?? $this->httpClient ?? HttpClientDiscovery::find(); + + return new Yandex($httpClient, $config['toponym'], $config['api_key']); + } + + protected static function configureOptionResolver(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'httplug_client' => null, + 'http_client' => null, + 'toponym' => null, + 'api_key' => null, + ]); + + $resolver->setAllowedTypes('httplug_client', ['object', 'null']); + $resolver->setAllowedTypes('http_client', ['object', 'null']); + $resolver->setAllowedTypes('toponym', ['string', 'null']); + $resolver->setAllowedTypes('api_key', ['string', 'null']); + + $resolver->setDeprecated('httplug_client', 'willdurand/geocoder-bundle', '5.19', 'The option "httplug_client" is deprecated, use "http_client" instead.'); + } +} diff --git a/src/Validator/Constraint/Address.php b/src/Validator/Constraint/Address.php new file mode 100644 index 00000000..29a04847 --- /dev/null +++ b/src/Validator/Constraint/Address.php @@ -0,0 +1,63 @@ + + */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class Address extends Constraint +{ + public const INVALID_ADDRESS_ERROR = '2243aa07-2ea7-4eb7-962c-6a9586593f2c'; + + protected const ERROR_NAMES = [ + self::INVALID_ADDRESS_ERROR => 'INVALID_ADDRESS_ERROR', + ]; + + /** + * @var array + * + * @deprecated since BazingaGeocoderBundle 5.17, use const ERROR_NAMES instead + */ + protected static $errorNames = self::ERROR_NAMES; + + /** + * @var string + */ + public $service = AddressValidator::class; + + /** + * @var string + */ + public $message = 'Address {{ address }} is not valid.'; + + /** + * @param string[]|null $options + */ + public function __construct(?array $options = null, ?string $message = null, ?array $groups = null, $payload = null) + { + parent::__construct($options, $groups, $payload); + + $this->message = $message ?? $this->message; + } + + public function validatedBy(): string + { + return $this->service; + } +} diff --git a/src/Validator/Constraint/AddressValidator.php b/src/Validator/Constraint/AddressValidator.php new file mode 100644 index 00000000..a01ff13a --- /dev/null +++ b/src/Validator/Constraint/AddressValidator.php @@ -0,0 +1,81 @@ + + */ +class AddressValidator extends ConstraintValidator +{ + /** + * @var Provider + */ + protected $addressGeocoder; + + public function __construct(Provider $addressGeocoder) + { + $this->addressGeocoder = $addressGeocoder; + } + + /** + * @param mixed $value + * + * @return void + */ + public function validate($value, Constraint $constraint) + { + if (!$constraint instanceof Address) { + throw new UnexpectedTypeException($constraint, Address::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { + throw new UnexpectedValueException($value, 'string'); + } + + $value = (string) $value; + + try { + $collection = $this->addressGeocoder->geocodeQuery(GeocodeQuery::create($value)); + + if ($collection->isEmpty()) { + $this->buildViolation($constraint, $value); + } + } catch (Exception $e) { + $this->buildViolation($constraint, $value); + } + } + + /** + * @return void + */ + private function buildViolation(Address $constraint, string $address) + { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ address }}', $this->formatValue($address)) + ->setInvalidValue($address) + ->setCode(Address::INVALID_ADDRESS_ERROR) + ->addViolation(); + } +} diff --git a/templates/Collector/geocoder.html.twig b/templates/Collector/geocoder.html.twig new file mode 100644 index 00000000..3618d968 --- /dev/null +++ b/templates/Collector/geocoder.html.twig @@ -0,0 +1,136 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block toolbar %} + {% set queryLabel = collector.queries|length == 1 ? 'query' : 'queries' %} + {% if collector.queries|length > 0 %} + {% set icon %} + {{ include('@BazingaGeocoder/Collector/icon.svg') }} + {{ collector.queries|length }} + {{ queryLabel }} in + {{ collector.totalDuration|number_format }} + ms + {% endset %} + {% set text %} +
+ {{ collector.queries|length }} {{ queryLabel }} +
+
+ + + + + + + + + + {% for query in collector.queries %} + + + + + + {% endfor %} + +
ProviderQueryTime
{{ query.providerName }}{{ query.queryString }}{{ query.duration == 0 ? 'n/a' : query.duration|number_format ~ ' ms'}}
+
+ {% endset %} + + {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': profiler_url } %} +{% endif %} +{% endblock %} + +{% block menu %} + {# This left-hand menu appears when using the full-screen profiler. #} + + + {{ include('@BazingaGeocoder/Collector/icon.svg') }} + + Geocoder + +{% endblock %} + +{% block panel %} + +

Geocoder

+ +
+ {% for provider in collector.providers %} +
+

{{ provider }} {{ collector.providerQueries(provider)|length }}

+ +
+

+ These queries are executed by a provider named "{{ provider }}". +

+ + + + + + + + + + + + + {% for query in collector.providerQueries(provider) %} + + + + + + + + + + + + {% if query.result.message is defined %} + + {% else %} + + {% endif %} + + + {% endfor %} + +
QueryLocaleResultDuration
+ # {{ loop.index }} + + + {{ query.queryString }} + + + + {{ query.query.locale is not null ? query.query.locale : 'null' }} + + + + {% if query.result.message is defined %} + Exception + {% else %} + {{ query.resultCount }} Result(s) + {% endif %} + + + + {{ query.duration|number_format }} ms +
{{ profiler_dump(query.query, maxDepth=1) }}{{ profiler_dump(query.result, maxDepth=1) }}{{ profiler_dump(query.result, maxDepth=3) }}
+ +
+
+ {% else %} +
+

No queries were executed.

+
+ {% endfor %} +
+{% endblock %} diff --git a/templates/Collector/icon.svg b/templates/Collector/icon.svg new file mode 100644 index 00000000..4ca756dc --- /dev/null +++ b/templates/Collector/icon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/tests/Command/GeocodeCommandTest.php b/tests/Command/GeocodeCommandTest.php new file mode 100644 index 00000000..203ef8e9 --- /dev/null +++ b/tests/Command/GeocodeCommandTest.php @@ -0,0 +1,79 @@ + + */ +final class GeocodeCommandTest extends TestCase +{ + private static $address = '10 rue Gambetta, Paris, France'; + + public function testExecute(): void + { + $coordinates = new Coordinates(1, 2); + $country = new Country('France', 'FR'); + $address = Address::createFromArray([ + 'coordinates' => $coordinates, + 'streetNumber' => '10', + 'streetName' => 'rue Gambetta', + 'zipCode' => '75020', + 'locality' => 'Paris', + 'countryName' => $country->getName(), + 'countryCode' => $country->getCode(), + ]); + + $geocoder = $this->createMock(ProviderAggregator::class); + $query = GeocodeQuery::create(self::$address); + $geocoder->expects($this->once()) + ->method('geocodeQuery') + ->with($query) + ->willReturn(new AddressCollection([$address])); + + $container = $this->createMock(Container::class); + + $kernel = $this->createMock(Kernel::class); + + $kernel->expects($this->any()) + ->method('getContainer') + ->willReturn($container); + + $kernel->expects($this->any()) + ->method('getBundles') + ->willReturn([]); + + $app = new Application($kernel); + $app->add((new GeocodeCommand($geocoder))->setName('geocoder:geocode')); + + $command = $app->find('geocoder:geocode'); + + $tester = new CommandTester($command); + $tester->execute([ + 'command' => 'geocoder:geocode', + 'address' => self::$address, + ]); + } +} diff --git a/tests/DependencyInjection/Compiler/AddProvidersPassTest.php b/tests/DependencyInjection/Compiler/AddProvidersPassTest.php new file mode 100644 index 00000000..7144744d --- /dev/null +++ b/tests/DependencyInjection/Compiler/AddProvidersPassTest.php @@ -0,0 +1,49 @@ +compilerPass = new AddProvidersPass(); + } + + public function testRegistersProviders(): void + { + $containerBuilder = new ContainerBuilder(); + $containerBuilder->setDefinition(ProviderAggregator::class, new Definition(ProviderAggregator::class)); + + $bing = $containerBuilder->setDefinition('bing_maps', new Definition(BingMaps::class, [new Client(), 'apikey'])); + $bing->addTag('bazinga_geocoder.provider'); + + $this->compilerPass->process($containerBuilder); + + /** @var ProviderAggregator $providerAggregator */ + $providerAggregator = $containerBuilder->get(ProviderAggregator::class); + $providers = $providerAggregator->getProviders(); + + self::assertArrayHasKey('bing_maps', $providers); + self::assertInstanceOf(BingMaps::class, $providers['bing_maps']); + } +} diff --git a/tests/DependencyInjection/Compiler/FactoryValidatorPassTest.php b/tests/DependencyInjection/Compiler/FactoryValidatorPassTest.php new file mode 100644 index 00000000..0b7f3f7f --- /dev/null +++ b/tests/DependencyInjection/Compiler/FactoryValidatorPassTest.php @@ -0,0 +1,87 @@ +compilerPass = new FactoryValidatorPass(); + $this->factoryId = 'dummy_factory_id'; + $this->compilerPass::addFactoryServiceId($this->factoryId); + } + + protected function tearDown(): void + { + $reflection = new \ReflectionObject($this->compilerPass); + $prop = $reflection->getProperty('factoryServiceIds'); + $prop->setAccessible(true); + $prop->setValue(null, []); + } + + public function testProcessThrows(): void + { + $this->expectException(ServiceNotFoundException::class); + $this->expectExceptionMessage("Factory with ID \"$this->factoryId\" could not be found"); + + $container = $this->createMock(ContainerBuilder::class); + $container->expects($this->once()) + ->method('hasAlias') + ->with($this->factoryId) + ->willReturn(false); + $container->expects($this->once()) + ->method('hasDefinition') + ->with($this->factoryId) + ->willReturn(false); + + $this->compilerPass->process($container); + } + + public function testProcessDoesntThrowIfAliasExists(): void + { + $container = $this->createMock(ContainerBuilder::class); + $container->expects($this->once()) + ->method('hasAlias') + ->with($this->factoryId) + ->willReturn(true); + $container->expects($this->never()) + ->method('hasDefinition') + ->with($this->factoryId) + ->willReturn(false); + + $this->compilerPass->process($container); + } + + public function testProcessDoesntThrowIfDefinitionExists(): void + { + $container = $this->createMock(ContainerBuilder::class); + $container->expects($this->once()) + ->method('hasAlias') + ->with($this->factoryId) + ->willReturn(false); + $container->expects($this->once()) + ->method('hasDefinition') + ->with($this->factoryId) + ->willReturn(true); + + $this->compilerPass->process($container); + } +} diff --git a/tests/DependencyInjection/Compiler/ProfilerPassTest.php b/tests/DependencyInjection/Compiler/ProfilerPassTest.php new file mode 100644 index 00000000..05bba924 --- /dev/null +++ b/tests/DependencyInjection/Compiler/ProfilerPassTest.php @@ -0,0 +1,47 @@ +compilerPass = new ProfilerPass(); + } + + public function testRegistersProviders(): void + { + $geocoderDataCollectorDefinition = new Definition(GeocoderDataCollector::class); + + $containerBuilder = new ContainerBuilder(); + $containerBuilder->setDefinition(GeocoderDataCollector::class, $geocoderDataCollectorDefinition); + + $bing = $containerBuilder->setDefinition('geocoder_profiling', new Definition(ProfilingPlugin::class, ['provider_id'])); + $bing->addTag('bazinga_geocoder.profiling_plugin'); + + $this->compilerPass->process($containerBuilder); + + self::assertTrue($geocoderDataCollectorDefinition->hasMethodCall('addInstance')); + self::assertInstanceOf(Reference::class, $geocoderDataCollectorDefinition->getMethodCalls()[0][1][0]); + } +} diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php new file mode 100644 index 00000000..858f259d --- /dev/null +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,53 @@ + + */ +final class ConfigurationTest extends TestCase +{ + public function testGetConfigTreeBuilder(): void + { + $config = Yaml::parseFile(__DIR__.'/Fixtures/config.yml'); + + $configuration = new Configuration(true); + $treeBuilder = $configuration->getConfigTreeBuilder(); + $processor = new Processor(); + + $config = $processor->process($treeBuilder->buildTree(), $config); + + self::assertTrue($config['profiling']['enabled']); + self::assertTrue($config['fake_ip']['enabled']); + self::assertSame('192.168.99.1', $config['fake_ip']['local_ip']); + self::assertSame('33.33.33.11', $config['fake_ip']['ip']); + } + + public function testGetConfigTreeBuilderNoDebug(): void + { + $config = Yaml::parseFile(__DIR__.'/Fixtures/config.yml'); + + $configuration = new Configuration(false); + $treeBuilder = $configuration->getConfigTreeBuilder(); + $processor = new Processor(); + + $config = $processor->process($treeBuilder->buildTree(), $config); + + self::assertFalse($config['profiling']['enabled']); + } +} diff --git a/tests/DependencyInjection/Fixtures/config.yml b/tests/DependencyInjection/Fixtures/config.yml new file mode 100644 index 00000000..87de3cc4 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/config.yml @@ -0,0 +1,10 @@ +bazinga_geocoder: + fake_ip: + local_ip: 192.168.99.1 + ip: 33.33.33.11 + providers: + bing_maps: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory + options: + api_key: 123 + locale: en_US diff --git a/Tests/DependencyInjection/Fixtures/maxmind.dat b/tests/DependencyInjection/Fixtures/maxmind.dat similarity index 100% rename from Tests/DependencyInjection/Fixtures/maxmind.dat rename to tests/DependencyInjection/Fixtures/maxmind.dat diff --git a/tests/Functional/BundleInitializationTest.php b/tests/Functional/BundleInitializationTest.php new file mode 100644 index 00000000..4e91031c --- /dev/null +++ b/tests/Functional/BundleInitializationTest.php @@ -0,0 +1,204 @@ +addTestBundle(BazingaGeocoderBundle::class); + $kernel->handleOptions($options); + + return $kernel; + } + + public function testInitBundle(): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + }]); + + $container = self::getContainer(); + + // Test if services exists + self::assertTrue($container->has(ProviderAggregator::class)); + $service = $container->get(ProviderAggregator::class); + self::assertInstanceOf(ProviderAggregator::class, $service); + } + + public function testBundleWithOneProviderConfiguration(): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/simple.yml'); + }]); + + $container = self::getContainer(); + + self::assertTrue($container->has('bazinga_geocoder.provider.acme')); + $service = $container->get('bazinga_geocoder.provider.acme'); + self::assertInstanceOf(PluginProvider::class, $service); + self::assertInstanceOf(GoogleMaps::class, NSA::getProperty($service, 'provider')); + } + + public function testBundleWithCachedProvider(): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/cache.yml'); + }]); + + $container = self::getContainer(); + + self::assertTrue($container->has('bazinga_geocoder.provider.acme')); + $service = $container->get('bazinga_geocoder.provider.acme'); + self::assertInstanceOf(PluginProvider::class, $service); + $plugins = NSA::getProperty($service, 'plugins'); + self::assertNotEmpty($plugins); + self::assertInstanceOf(CachePlugin::class, $plugins[0]); + } + + public function testCacheLifetimeCanBeNull(): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/cache_without_lifetime.yml'); + }]); + + $container = self::getContainer(); + + self::assertTrue($container->has('bazinga_geocoder.provider.acme')); + + /** @var PluginProvider $service */ + $service = $container->get('bazinga_geocoder.provider.acme'); + self::assertInstanceOf(PluginProvider::class, $service); + + $plugins = NSA::getProperty($service, 'plugins'); + self::assertCount(1, $plugins); + + $cachePlugin = array_shift($plugins); + self::assertInstanceOf(CachePlugin::class, $cachePlugin); + + $cacheLifeTime = NSA::getProperty($cachePlugin, 'lifetime'); + self::assertNull($cacheLifeTime); + } + + public function testBundleWithPluginsYml(): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/service_plugin.yml'); + }]); + + $container = self::getContainer(); + + self::assertTrue($container->has('bazinga_geocoder.provider.acme')); + $service = $container->get('bazinga_geocoder.provider.acme'); + self::assertInstanceOf(PluginProvider::class, $service); + $plugins = NSA::getProperty($service, 'plugins'); + self::assertCount(3, $plugins); + self::assertInstanceOf(LoggerPlugin::class, $plugins[0]); + } + + public function testBundleWithPluginXml(): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/service_plugin.xml'); + }]); + + $container = self::getContainer(); + + self::assertTrue($container->has('bazinga_geocoder.provider.acme')); + $service = $container->get('bazinga_geocoder.provider.acme'); + self::assertInstanceOf(PluginProvider::class, $service); + $plugins = NSA::getProperty($service, 'plugins'); + self::assertNotEmpty($plugins); + self::assertInstanceOf(LoggerPlugin::class, $plugins[0]); + } + + public function testBundleHasRegisteredDumpers(): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + }]); + + $container = self::getContainer(); + + self::assertTrue($container->has(GeoArray::class)); + self::assertTrue($container->has(GeoJson::class)); + self::assertTrue($container->has(Gpx::class)); + self::assertTrue($container->has(Kml::class)); + self::assertTrue($container->has(Wkb::class)); + self::assertTrue($container->has(Wkt::class)); + } +} diff --git a/tests/Functional/CustomTestKernel.php b/tests/Functional/CustomTestKernel.php new file mode 100644 index 00000000..e32e3fb2 --- /dev/null +++ b/tests/Functional/CustomTestKernel.php @@ -0,0 +1,219 @@ +shutdown(); + $this->warmupDir = $warmupDir; + $this->boot(); + } + + /* + * Needed, otherwise the used cache is different on each kernel boot, which is a big issue in PluginInteractionTest + */ + public function getCacheDir(): string + { + return realpath(sys_get_temp_dir()).'/NyholmBundleTest/cachePluginInteractionTest'; + } + + /** + * Returns the kernel parameters. + */ + protected function getKernelParameters(): array + { + $bundles = []; + $bundlesMetadata = []; + + foreach ($this->bundles as $name => $bundle) { + $bundles[$name] = \get_class($bundle); + $bundlesMetadata[$name] = [ + 'path' => $bundle->getPath(), + 'namespace' => $bundle->getNamespace(), + ]; + } + + return [ + 'kernel.project_dir' => realpath($this->getProjectDir()) ?: $this->getProjectDir(), + 'kernel.environment' => $this->environment, + 'kernel.runtime_environment' => '%env(default:kernel.environment:APP_RUNTIME_ENV)%', + 'kernel.debug' => $this->debug, + 'kernel.build_dir' => realpath($buildDir = $this->warmupDir ?: $this->getBuildDir()) ?: $buildDir, + 'kernel.cache_dir' => realpath($cacheDir = ($this->getCacheDir() === $this->getBuildDir() ? ($this->warmupDir ?: $this->getCacheDir()) : $this->getCacheDir())) ?: $cacheDir, + 'kernel.logs_dir' => realpath($this->getLogDir()) ?: $this->getLogDir(), + 'kernel.bundles' => $bundles, + 'kernel.bundles_metadata' => $bundlesMetadata, + 'kernel.charset' => $this->getCharset(), + 'kernel.container_class' => $this->getContainerClass(), + ]; + } + + /** + * @internal + */ + public function setAnnotatedClassCache(array $annotatedClasses): void + { + file_put_contents(($this->warmupDir ?: $this->getBuildDir()).'/annotations.map', sprintf('getContainerClass(); + $buildDir = $this->warmupDir ?: $this->getBuildDir(); + $cache = new ConfigCache($buildDir.'/'.$class.'.php', $this->debug); + $cachePath = $cache->getPath(); + + // Silence E_WARNING to ignore "include" failures - don't use "@" to prevent silencing fatal errors + $errorLevel = error_reporting(\E_ALL ^ \E_WARNING); + + try { + if (false && \is_object($this->container = include $cachePath) + && (!$this->debug || (self::$freshCache[$cachePath] ?? $cache->isFresh())) + ) { + self::$freshCache[$cachePath] = true; + $this->container->set('kernel', $this); + error_reporting($errorLevel); + + return; + } + } catch (\Throwable $e) { + } + + try { + is_dir($buildDir) || mkdir($buildDir, 0777, true); + + if ($lock = fopen($cachePath.'.lock', 'w')) { + if (!flock($lock, \LOCK_EX | \LOCK_NB, $wouldBlock) && !flock($lock, $wouldBlock ? \LOCK_SH : \LOCK_EX)) { + fclose($lock); + $lock = null; + } else { + $this->container = null; + } + } + } catch (\Throwable $e) { + } finally { + error_reporting($errorLevel); + } + + if ($collectDeprecations = $this->debug && !\defined('PHPUNIT_COMPOSER_INSTALL')) { + $collectedLogs = []; + $previousHandler = set_error_handler(function ($type, $message, $file, $line) use (&$collectedLogs, &$previousHandler) { + if (\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type) { + return $previousHandler ? $previousHandler($type, $message, $file, $line) : false; + } + + if (isset($collectedLogs[$message])) { + ++$collectedLogs[$message]['count']; + + return null; + } + + $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 5); + // Clean the trace by removing first frames added by the error handler itself. + for ($i = 0; isset($backtrace[$i]); ++$i) { + if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) { + $backtrace = \array_slice($backtrace, 1 + $i); + break; + } + } + for ($i = 0; isset($backtrace[$i]); ++$i) { + if (!isset($backtrace[$i]['file'], $backtrace[$i]['line'], $backtrace[$i]['function'])) { + continue; + } + if (!isset($backtrace[$i]['class']) && 'trigger_deprecation' === $backtrace[$i]['function']) { + $file = $backtrace[$i]['file']; + $line = $backtrace[$i]['line']; + $backtrace = \array_slice($backtrace, 1 + $i); + break; + } + } + + // Remove frames added by DebugClassLoader. + for ($i = \count($backtrace) - 2; 0 < $i; --$i) { + if ($backtrace[$i]['class'] ?? null === DebugClassLoader::class) { + $backtrace = [$backtrace[$i + 1]]; + break; + } + } + + $collectedLogs[$message] = [ + 'type' => $type, + 'message' => $message, + 'file' => $file, + 'line' => $line, + 'trace' => [$backtrace[0]], + 'count' => 1, + ]; + + return null; + }); + } + + try { + $container = null; + $container = $this->buildContainer(); + $container->compile(); + } finally { + if ($collectDeprecations) { + restore_error_handler(); + + @file_put_contents($buildDir.'/'.$class.'Deprecations.log', serialize(array_values($collectedLogs))); + @file_put_contents($buildDir.'/'.$class.'Compiler.log', null !== $container ? implode("\n", $container->getCompiler()->getLog()) : ''); + } + } + + $this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass()); + + if ($lock) { + flock($lock, \LOCK_UN); + fclose($lock); + } + + $this->container = require $cachePath; + $this->container->set('kernel', $this); + + $preload = $this instanceof WarmableInterface ? (array) $this->warmUp($this->container->getParameter('kernel.cache_dir')) : []; + + if ($this->container->has('cache_warmer')) { + $preload = array_merge($preload, (array) $this->container->get('cache_warmer')->warmUp($this->container->getParameter('kernel.cache_dir'))); + } + + if ($preload && method_exists(Preloader::class, 'append') && file_exists($preloadFile = $buildDir.'/'.$class.'.preload.php')) { + Preloader::append($preloadFile, $preload); + } + } +} diff --git a/tests/Functional/Fixtures/Entity/DummyWithEmptyProperty.php b/tests/Functional/Fixtures/Entity/DummyWithEmptyProperty.php new file mode 100644 index 00000000..5431a311 --- /dev/null +++ b/tests/Functional/Fixtures/Entity/DummyWithEmptyProperty.php @@ -0,0 +1,70 @@ +_address = $address; + } + + /** + * @Address + */ + #[Address] + public function getAddress() + { + return $this->_address; + } + + public function getLatitude() + { + return $this->latitude; + } + + public function setLatitude($latitude) + { + $this->latitude = $latitude; + } + + public function getLongitude() + { + return $this->longitude; + } + + public function setLongitude($longitude) + { + $this->longitude = $longitude; + } +} diff --git a/tests/Functional/Fixtures/Entity/DummyWithInvalidGetter.php b/tests/Functional/Fixtures/Entity/DummyWithInvalidGetter.php new file mode 100644 index 00000000..63d3e8ce --- /dev/null +++ b/tests/Functional/Fixtures/Entity/DummyWithInvalidGetter.php @@ -0,0 +1,101 @@ +_address = $address; + } + + /** + * @Address + */ + #[Address] + public function getAddress($requiredParameter) + { + return $this->_address; + } + + public function getLatitude() + { + return $this->latitude; + } + + public function setLatitude($latitude) + { + $this->latitude = $latitude; + } + + public function getLongitude() + { + return $this->longitude; + } + + public function setLongitude($longitude) + { + $this->longitude = $longitude; + } +} diff --git a/tests/Functional/Fixtures/Entity/DummyWithProperty.php b/tests/Functional/Fixtures/Entity/DummyWithProperty.php new file mode 100644 index 00000000..eb88e3ef --- /dev/null +++ b/tests/Functional/Fixtures/Entity/DummyWithProperty.php @@ -0,0 +1,72 @@ + + */ +final class GeocoderListenerTest extends KernelTestCase +{ + protected function tearDown(): void + { + $em = self::getContainer()->get('doctrine.orm.entity_manager'); + + $tool = new SchemaTool($em); + $tool->dropSchema($em->getMetadataFactory()->getAllMetadata()); + } + + protected static function getKernelClass(): string + { + return TestKernel::class; + } + + protected static function createKernel(array $options = []): KernelInterface + { + /** + * @var TestKernel $kernel + */ + $kernel = parent::createKernel($options); + $kernel->addTestBundle(DoctrineBundle::class); + $kernel->addTestBundle(BazingaGeocoderBundle::class); + if (defined(ConnectionFactory::class.'::DEFAULT_SCHEME_MAP')) { + $kernel->addTestConfig(static function (ContainerBuilder $container) { + $container->prependExtensionConfig('doctrine', [ + 'orm' => [ + 'report_fields_where_declared' => true, + ], + ]); + + if (method_exists(Configuration::class, 'setLazyGhostObjectEnabled') && Kernel::VERSION_ID >= 60100) { + $container->prependExtensionConfig('doctrine', [ + 'orm' => [ + 'enable_lazy_ghost_objects' => true, + ], + ]); + } + + if (class_exists(EntityValueResolver::class)) { + $container->prependExtensionConfig('doctrine', [ + 'orm' => [ + 'controller_resolver' => [ + 'auto_mapping' => false, + ], + ], + ]); + } + }); + } + $kernel->handleOptions($options); + + return $kernel; + } + + public function testPersistForProperty(): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/listener.yml'); + $kernel->addTestConfig(__DIR__.'/config/listener_'.(PHP_VERSION_ID >= 80000 ? 'php8' : 'php7').'.yml'); + }]); + + $container = self::getContainer(); + + $httpClient = $container->get(Client::class); + $httpClient->on(new RequestMatcher(), function (RequestInterface $request) { + if ('https://nominatim.openstreetmap.org/search?format=jsonv2&q=Berlin%2C%20Germany&addressdetails=1&extratags=1&limit=5' === (string) $request->getUri()) { + $stream = $this->createMock(StreamInterface::class); + $stream->expects(self::once()) + ->method('__toString') + ->willReturn('[{"place_id":159647018,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":62422,"lat":"52.5170365","lon":"13.3888599","category":"boundary","type":"administrative","place_rank":8,"importance":0.7875390282491362,"addresstype":"city","name":"Berlin","display_name":"Berlin, Deutschland","address":{"city":"Berlin","ISO3166-2-lvl4":"DE-BE","country":"Deutschland","country_code":"de"},"extratags":{"ele": "35", "email": "info@berlin.de", "place": "city", "capital": "yes", "website": "http://www.berlin.de", "de:place": "city", "ref:nuts": "DE3;DE30;DE300", "wikidata": "Q64", "wikipedia": "de:Berlin", "population": "3769962", "ref:LOCODE": "DEBER", "ref:nuts:1": "DE3", "ref:nuts:2": "DE30", "ref:nuts:3": "DE300", "state_code": "BE", "name:prefix": "Land und Kreisfreie Stadt", "linked_place": "city", "official_status": "Land", "contact:facebook": "http://www.facebook.com/Berlin", "name:prefix:city": "Kreisfreie Stadt", "openGeoDB:loc_id": "14356", "capital_ISO3166-1": "yes", "name:prefix:state": "Land", "source:population": "https://download.statistik-berlin-brandenburg.de/fa93e3bd19a2e885/a5ecfb2fff6a/SB_A01-05-00_2020h02_BE.pdf", "license_plate_code": "B", "official_status:de": "Land", "official_status:en": "State", "official_status:ru": "земля", "geographical_region": "Barnim;Berliner Urstromtal;Teltow;Nauener Platte", "blind:description:de": "Auf www.berlinfuerblinde.de gibt es einen kostenlosen Audioguide und weitere Informationen.", "de:regionalschluessel": "110000000000", "openGeoDB:postal_codes": "10178,10115,10117,10119,10179,10243,10245,10247,10249,10315,10317,10318,10319,10365,10367,10369,10405,10407,10409,10435,10437,10439,10551,10553,10555,10557,10559,10585,10587,10589,10623,10625,10627,10629,10707,10709,10711,10713,10715,10717,10719,10777,10", "report_problems:website": "https://ordnungsamt.berlin.de/", "TMC:cid_58:tabcd_1:Class": "Area", "openGeoDB:license_plate_code": "B", "TMC:cid_58:tabcd_1:LCLversion": "12.0", "openGeoDB:telephone_area_code": "030", "TMC:cid_58:tabcd_1:LocationCode": "266", "de:amtlicher_gemeindeschluessel": "11000000", "openGeoDB:community_identification_number": "11000000"},"boundingbox":["52.3382448","52.6755087","13.0883450","13.7611609"]}]'); + + $response = $this->createMock(ResponseInterface::class); + $response->expects(self::once()) + ->method('getStatusCode') + ->willReturn(200); + $response->expects(self::once()) + ->method('getBody') + ->willReturn($stream); + + return $response; + } + + if ('https://nominatim.openstreetmap.org/search?format=jsonv2&q=Paris%2C%20France&addressdetails=1&extratags=1&limit=5' === (string) $request->getUri()) { + $stream = $this->createMock(StreamInterface::class); + $stream->expects(self::once()) + ->method('__toString') + ->willReturn('[{"place_id":115350921,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":7444,"lat":"48.8588897","lon":"2.3200410217200766","category":"boundary","type":"administrative","place_rank":15,"importance":0.8317101715588673,"addresstype":"suburb","name":"Paris","display_name":"Paris, Île-de-France, France métropolitaine, France","address":{"suburb":"Paris","city_district":"Paris","city":"Paris","ISO3166-2-lvl6":"FR-75","state":"Île-de-France","ISO3166-2-lvl4":"FR-IDF","region":"France métropolitaine","country":"France","country_code":"fr"},"extratags":{"capital": "yes", "wikidata": "Q90", "ref:INSEE": "75056", "wikipedia": "fr:Paris", "population": "2187526", "ref:FR:MGP": "T1", "source:population": "INSEE 2020"},"boundingbox":["48.8155755","48.9021560","2.2241220","2.4697602"]},{"place_id":114827617,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":71525,"lat":"48.8534951","lon":"2.3483915","category":"boundary","type":"administrative","place_rank":12,"importance":0.8317101715588673,"addresstype":"city","name":"Paris","display_name":"Paris, Île-de-France, France métropolitaine, France","address":{"city":"Paris","ISO3166-2-lvl6":"FR-75","state":"Île-de-France","ISO3166-2-lvl4":"FR-IDF","region":"France métropolitaine","country":"France","country_code":"fr"},"extratags":{"rank": "0", "capital": "yes", "ref:nuts": "FR101", "wikidata": "Q90", "ref:INSEE": "75", "wikipedia": "fr:Paris", "is_capital": "country", "population": "2165423", "ref:nuts:3": "FR101", "linked_place": "city", "source:name:oc": "ieo-bdtopoc", "contact:website": "http://www.paris.fr", "population:date": "2019", "capital_ISO3166-1": "yes", "source:population": "INSEE 2022"},"boundingbox":["48.8155755","48.9021560","2.2241220","2.4697602"]},{"place_id":114994164,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":1641193,"lat":"48.8588897","lon":"2.3200410217200766","category":"boundary","type":"administrative","place_rank":14,"importance":0.4283953917728152,"addresstype":"city_district","name":"Paris","display_name":"Paris, Île-de-France, France métropolitaine, France","address":{"city_district":"Paris","city":"Paris","ISO3166-2-lvl6":"FR-75","state":"Île-de-France","ISO3166-2-lvl4":"FR-IDF","region":"France métropolitaine","country":"France","country_code":"fr"},"extratags":{"wikidata": "Q2863958", "ref:INSEE": "751", "wikipedia": "fr:Arrondissement de Paris"},"boundingbox":["48.8155755","48.9021560","2.2241220","2.4697602"]}]'); + + $response = $this->createMock(ResponseInterface::class); + $response->expects(self::once()) + ->method('getStatusCode') + ->willReturn(200); + $response->expects(self::once()) + ->method('getBody') + ->willReturn($stream); + + return $response; + } + + self::fail(sprintf('Unexpected http call "%s %s".', $request->getMethod(), (string) $request->getUri())); + }); + + $dummy = new DummyWithProperty(); + $dummy->address = 'Berlin, Germany'; + + $em = $container->get('doctrine.orm.entity_manager'); + + $tool = new SchemaTool($em); + $tool->createSchema($em->getMetadataFactory()->getAllMetadata()); + + $em->persist($dummy); + $em->flush(); + + self::assertNotNull($dummy->latitude); + self::assertNotNull($dummy->longitude); + + $clone = clone $dummy; + $dummy->address = 'Paris, France'; + + $em->persist($dummy); + $em->flush(); + + self::assertNotEquals($clone->latitude, $dummy->latitude); + self::assertNotEquals($clone->longitude, $dummy->longitude); + } + + public function testPersistForGetter(): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/listener.yml'); + $kernel->addTestConfig(__DIR__.'/config/listener_'.(PHP_VERSION_ID >= 80000 ? 'php8' : 'php7').'.yml'); + }]); + + $container = self::getContainer(); + + $httpClient = $container->get(Client::class); + $httpClient->on(new RequestMatcher(), function (RequestInterface $request) { + if ('https://nominatim.openstreetmap.org/search?format=jsonv2&q=Berlin%2C%20Germany&addressdetails=1&extratags=1&limit=5' === (string) $request->getUri()) { + $stream = $this->createMock(StreamInterface::class); + $stream->expects(self::once()) + ->method('__toString') + ->willReturn('[{"place_id":159647018,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":62422,"lat":"52.5170365","lon":"13.3888599","category":"boundary","type":"administrative","place_rank":8,"importance":0.7875390282491362,"addresstype":"city","name":"Berlin","display_name":"Berlin, Deutschland","address":{"city":"Berlin","ISO3166-2-lvl4":"DE-BE","country":"Deutschland","country_code":"de"},"extratags":{"ele": "35", "email": "info@berlin.de", "place": "city", "capital": "yes", "website": "http://www.berlin.de", "de:place": "city", "ref:nuts": "DE3;DE30;DE300", "wikidata": "Q64", "wikipedia": "de:Berlin", "population": "3769962", "ref:LOCODE": "DEBER", "ref:nuts:1": "DE3", "ref:nuts:2": "DE30", "ref:nuts:3": "DE300", "state_code": "BE", "name:prefix": "Land und Kreisfreie Stadt", "linked_place": "city", "official_status": "Land", "contact:facebook": "http://www.facebook.com/Berlin", "name:prefix:city": "Kreisfreie Stadt", "openGeoDB:loc_id": "14356", "capital_ISO3166-1": "yes", "name:prefix:state": "Land", "source:population": "https://download.statistik-berlin-brandenburg.de/fa93e3bd19a2e885/a5ecfb2fff6a/SB_A01-05-00_2020h02_BE.pdf", "license_plate_code": "B", "official_status:de": "Land", "official_status:en": "State", "official_status:ru": "земля", "geographical_region": "Barnim;Berliner Urstromtal;Teltow;Nauener Platte", "blind:description:de": "Auf www.berlinfuerblinde.de gibt es einen kostenlosen Audioguide und weitere Informationen.", "de:regionalschluessel": "110000000000", "openGeoDB:postal_codes": "10178,10115,10117,10119,10179,10243,10245,10247,10249,10315,10317,10318,10319,10365,10367,10369,10405,10407,10409,10435,10437,10439,10551,10553,10555,10557,10559,10585,10587,10589,10623,10625,10627,10629,10707,10709,10711,10713,10715,10717,10719,10777,10", "report_problems:website": "https://ordnungsamt.berlin.de/", "TMC:cid_58:tabcd_1:Class": "Area", "openGeoDB:license_plate_code": "B", "TMC:cid_58:tabcd_1:LCLversion": "12.0", "openGeoDB:telephone_area_code": "030", "TMC:cid_58:tabcd_1:LocationCode": "266", "de:amtlicher_gemeindeschluessel": "11000000", "openGeoDB:community_identification_number": "11000000"},"boundingbox":["52.3382448","52.6755087","13.0883450","13.7611609"]}]'); + + $response = $this->createMock(ResponseInterface::class); + $response->expects(self::once()) + ->method('getStatusCode') + ->willReturn(200); + $response->expects(self::once()) + ->method('getBody') + ->willReturn($stream); + + return $response; + } + + if ('https://nominatim.openstreetmap.org/search?format=jsonv2&q=Paris%2C%20France&addressdetails=1&extratags=1&limit=5' === (string) $request->getUri()) { + $stream = $this->createMock(StreamInterface::class); + $stream->expects(self::once()) + ->method('__toString') + ->willReturn('[{"place_id":115350921,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":7444,"lat":"48.8588897","lon":"2.3200410217200766","category":"boundary","type":"administrative","place_rank":15,"importance":0.8317101715588673,"addresstype":"suburb","name":"Paris","display_name":"Paris, Île-de-France, France métropolitaine, France","address":{"suburb":"Paris","city_district":"Paris","city":"Paris","ISO3166-2-lvl6":"FR-75","state":"Île-de-France","ISO3166-2-lvl4":"FR-IDF","region":"France métropolitaine","country":"France","country_code":"fr"},"extratags":{"capital": "yes", "wikidata": "Q90", "ref:INSEE": "75056", "wikipedia": "fr:Paris", "population": "2187526", "ref:FR:MGP": "T1", "source:population": "INSEE 2020"},"boundingbox":["48.8155755","48.9021560","2.2241220","2.4697602"]},{"place_id":114827617,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":71525,"lat":"48.8534951","lon":"2.3483915","category":"boundary","type":"administrative","place_rank":12,"importance":0.8317101715588673,"addresstype":"city","name":"Paris","display_name":"Paris, Île-de-France, France métropolitaine, France","address":{"city":"Paris","ISO3166-2-lvl6":"FR-75","state":"Île-de-France","ISO3166-2-lvl4":"FR-IDF","region":"France métropolitaine","country":"France","country_code":"fr"},"extratags":{"rank": "0", "capital": "yes", "ref:nuts": "FR101", "wikidata": "Q90", "ref:INSEE": "75", "wikipedia": "fr:Paris", "is_capital": "country", "population": "2165423", "ref:nuts:3": "FR101", "linked_place": "city", "source:name:oc": "ieo-bdtopoc", "contact:website": "http://www.paris.fr", "population:date": "2019", "capital_ISO3166-1": "yes", "source:population": "INSEE 2022"},"boundingbox":["48.8155755","48.9021560","2.2241220","2.4697602"]},{"place_id":114994164,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":1641193,"lat":"48.8588897","lon":"2.3200410217200766","category":"boundary","type":"administrative","place_rank":14,"importance":0.4283953917728152,"addresstype":"city_district","name":"Paris","display_name":"Paris, Île-de-France, France métropolitaine, France","address":{"city_district":"Paris","city":"Paris","ISO3166-2-lvl6":"FR-75","state":"Île-de-France","ISO3166-2-lvl4":"FR-IDF","region":"France métropolitaine","country":"France","country_code":"fr"},"extratags":{"wikidata": "Q2863958", "ref:INSEE": "751", "wikipedia": "fr:Arrondissement de Paris"},"boundingbox":["48.8155755","48.9021560","2.2241220","2.4697602"]}]'); + + $response = $this->createMock(ResponseInterface::class); + $response->expects(self::once()) + ->method('getStatusCode') + ->willReturn(200); + $response->expects(self::once()) + ->method('getBody') + ->willReturn($stream); + + return $response; + } + + self::fail(sprintf('Unexpected http call "%s %s".', $request->getMethod(), (string) $request->getUri())); + }); + + $em = $container->get('doctrine.orm.entity_manager'); + + $tool = new SchemaTool($em); + $tool->createSchema($em->getMetadataFactory()->getAllMetadata()); + + $dummy = new DummyWithGetter(); + $dummy->setAddress('Berlin, Germany'); + + $em->persist($dummy); + $em->flush(); + + self::assertNotNull($dummy->getLatitude()); + self::assertNotNull($dummy->getLongitude()); + + $clone = clone $dummy; + $dummy->setAddress('Paris, France'); + + $em->persist($dummy); + $em->flush(); + + self::assertNotEquals($clone->getLatitude(), $dummy->getLatitude()); + self::assertNotEquals($clone->getLongitude(), $dummy->getLongitude()); + } + + public function testPersistForInvalidGetter(): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/listener.yml'); + $kernel->addTestConfig(__DIR__.'/config/listener_'.(PHP_VERSION_ID >= 80000 ? 'php8' : 'php7').'.yml'); + }]); + + $container = self::getContainer(); + + $em = $container->get('doctrine.orm.entity_manager'); + + $tool = new SchemaTool($em); + $tool->createSchema($em->getMetadataFactory()->getAllMetadata()); + + $dummy = new DummyWithInvalidGetter(); + $dummy->setAddress('Berlin, Germany'); + + $em->persist($dummy); + + $this->expectException(\Exception::class); + + $em->flush(); + } + + public function testPersistForEmptyProperty(): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/listener.yml'); + $kernel->addTestConfig(__DIR__.'/config/listener_'.(PHP_VERSION_ID >= 80000 ? 'php8' : 'php7').'.yml'); + }]); + + $container = self::getContainer(); + + $em = $container->get('doctrine.orm.entity_manager'); + + $tool = new SchemaTool($em); + $tool->createSchema($em->getMetadataFactory()->getAllMetadata()); + + $dummy = new DummyWithEmptyProperty(); + $dummy->address = ''; + + $em->persist($dummy); + $em->flush(); + + self::assertNull($dummy->latitude); + self::assertNull($dummy->longitude); + } + + public function testDoesNotGeocodeIfAddressNotChanged(): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/listener.yml'); + $kernel->addTestConfig(__DIR__.'/config/listener_'.(PHP_VERSION_ID >= 80000 ? 'php8' : 'php7').'.yml'); + }]); + + $container = self::getContainer(); + + $httpRequests = 0; + $httpClient = $container->get(Client::class); + $httpClient->on(new RequestMatcher(), function (RequestInterface $request) use (&$httpRequests) { + $response = $this->createMock(ResponseInterface::class); + $response->expects(self::any()) + ->method('getStatusCode') + ->willReturn(200); + + if ('https://nominatim.openstreetmap.org/search?format=jsonv2&q=Frankfurt%2C%20Germany&addressdetails=1&extratags=1&limit=5' === (string) $request->getUri() && 0 === $httpRequests) { + $stream = $this->createMock(StreamInterface::class); + $stream->expects(self::once()) + ->method('__toString') + ->willReturn('[{"place_id":152571305,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":62400,"lat":"50.1106444","lon":"8.6820917","category":"boundary","type":"administrative","place_rank":12,"importance":0.6941325622496303,"addresstype":"city","name":"Frankfurt am Main","display_name":"Frankfurt am Main, Hessen, Deutschland","address":{"city":"Frankfurt am Main","state":"Hessen","ISO3166-2-lvl4":"DE-HE","country":"Deutschland","country_code":"de"},"extratags":{"ele": "112", "flag": "File:Flag of Frankfurt am Main.svg", "logo": "File:Frankfurt am Main logo.svg", "de:place": "city", "nickname": "Europastadt", "wikidata": "Q1794", "wikipedia": "de:Frankfurt am Main", "population": "701350", "ref:LOCODE": "DEFRA", "ref:nuts:3": "DE712", "border_type": "county", "name:prefix": "Stadt", "nickname:de": "Europastadt", "nickname:la": "Urbem Europaeam", "nickname:nl": "Bankfurt", "coat_of_arms": "File:Wappen Frankfurt am Main.svg", "linked_place": "city", "wikimedia_commons": "Category:Frankfurt am Main", "license_plate_code": "F", "de:regionalschluessel": "064120000000", "TMC:cid_58:tabcd_1:Class": "Area", "TMC:cid_58:tabcd_1:LCLversion": "9.00", "TMC:cid_58:tabcd_1:LocationCode": "414", "de:amtlicher_gemeindeschluessel": "06412000"},"boundingbox":["50.0153529","50.2271424","8.4727605","8.8004049"]},{"place_id":160849350,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":62523,"lat":"52.3412273","lon":"14.549452","category":"boundary","type":"administrative","place_rank":12,"importance":0.5626903004005709,"addresstype":"city","name":"Frankfurt (Oder)","display_name":"Frankfurt (Oder), Brandenburg, Deutschland","address":{"city":"Frankfurt (Oder)","state":"Brandenburg","ISO3166-2-lvl4":"DE-BB","country":"Deutschland","country_code":"de"},"extratags":{"ele": "28", "place": "city", "website": "https://www.frankfurt-oder.de/", "de:place": "city", "wikidata": "Q4024", "wikipedia": "de:Frankfurt (Oder)", "population": "61969", "ref:LOCODE": "DEFFO", "ref:nuts:3": "DE403", "name:prefix": "Kreisfreie Stadt", "linked_place": "town", "license_plate_code": "FF", "telephone_area_code": "0335", "de:regionalschluessel": "120530000000", "TMC:cid_58:tabcd_1:Class": "Area", "TMC:cid_58:tabcd_1:LCLversion": "8.00", "TMC:cid_58:tabcd_1:LocationCode": "415", "de:amtlicher_gemeindeschluessel": "12053000"},"boundingbox":["52.2528709","52.3980721","14.3948254","14.6013644"]}]'); + + $response->expects(self::once()) + ->method('getBody') + ->willReturn($stream); + } else { + $stream = $this->createMock(StreamInterface::class); + $stream->expects(self::once()) + ->method('__toString') + ->willReturn('[]'); + + $response->expects(self::once()) + ->method('getBody') + ->willReturn($stream); + } + + ++$httpRequests; + + return $response; + }); + + $em = $container->get('doctrine.orm.entity_manager'); + + $tool = new SchemaTool($em); + $tool->createSchema($em->getMetadataFactory()->getAllMetadata()); + + $dummy = new DummyWithProperty(); + $dummy->address = 'Frankfurt, Germany'; + + $em->persist($dummy); + $em->flush(); + + $dummy->latitude = 0; + $dummy->longitude = 0; + + $em->flush(); + + self::assertSame('Frankfurt, Germany', $dummy->address); + self::assertSame(0, $dummy->latitude); + self::assertSame(0, $dummy->longitude); + self::assertSame(1, $httpRequests); + } +} diff --git a/tests/Functional/Helper/CacheHelper.php b/tests/Functional/Helper/CacheHelper.php new file mode 100644 index 00000000..67df280a --- /dev/null +++ b/tests/Functional/Helper/CacheHelper.php @@ -0,0 +1,37 @@ += 80000) { + /** + * @internal + * + * @author Tobias Nyholm + */ + class CacheHelper implements CacheInterface + { + use CacheHelperV8; + } +} else { + /** + * @internal + * + * @author Tobias Nyholm + */ + class CacheHelper implements CacheInterface + { + use CacheHelperV7; + } +} diff --git a/tests/Functional/Helper/CacheHelperV7.php b/tests/Functional/Helper/CacheHelperV7.php new file mode 100644 index 00000000..fb50fb0d --- /dev/null +++ b/tests/Functional/Helper/CacheHelperV7.php @@ -0,0 +1,84 @@ + + */ +trait CacheHelperV7 +{ + /** + * @return mixed + */ + public function get($key, $default = null) + { + } + + /** + * @return bool + */ + public function set($key, $value, $ttl = null) + { + return true; + } + + /** + * @return bool + */ + public function delete($key) + { + return true; + } + + /** + * @return bool + */ + public function clear() + { + return true; + } + + /** + * @return iterable + */ + public function getMultiple($keys, $default = null) + { + return []; + } + + /** + * @return bool + */ + public function setMultiple($values, $ttl = null) + { + return true; + } + + /** + * @return bool + */ + public function deleteMultiple($keys) + { + return true; + } + + /** + * @return bool + */ + public function has($key) + { + return false; + } +} diff --git a/tests/Functional/Helper/CacheHelperV8.php b/tests/Functional/Helper/CacheHelperV8.php new file mode 100644 index 00000000..939ef94a --- /dev/null +++ b/tests/Functional/Helper/CacheHelperV8.php @@ -0,0 +1,60 @@ + + */ +trait CacheHelperV8 +{ + public function get(string $key, mixed $default = null): mixed + { + } + + public function set(string $key, mixed $value, int|\DateInterval|null $ttl = null): bool + { + return true; + } + + public function delete(string $key): bool + { + return true; + } + + public function clear(): bool + { + return true; + } + + public function getMultiple(iterable $keys, mixed $default = null): iterable + { + return []; + } + + public function setMultiple(iterable $values, int|\DateInterval|null $ttl = null): bool + { + return true; + } + + public function deleteMultiple(iterable $keys): bool + { + return true; + } + + public function has(string $key): bool + { + return false; + } +} diff --git a/tests/Functional/PluginInteractionTest.php b/tests/Functional/PluginInteractionTest.php new file mode 100644 index 00000000..77a6fe70 --- /dev/null +++ b/tests/Functional/PluginInteractionTest.php @@ -0,0 +1,83 @@ +addTestBundle(BazingaGeocoderBundle::class); + $kernel->handleOptions($options); + + return $kernel; + } + + public function testCachePluginUsesIpFromFakeIpPlugin(): void + { + $kernel = self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->setClearCacheAfterShutdown(false); + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/cache_symfony.yml'); + $kernel->addTestConfig(__DIR__.'/config/geo_plugin_fakeip_with_cache_cn.yml'); + }]); + $kernel->setClearCacheAfterShutdown(false); + $container = self::getContainer(); + + $geoPluginGeocoder = $container->get('bazinga_geocoder.provider.geoPlugin'); + $result = $geoPluginGeocoder->geocodeQuery(GeocodeQuery::create('::1')); + $country = $result->first()->getCountry()->getCode(); + self::assertEquals('CN', $country); + + $kernel = self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->setClearCacheAfterShutdown(false); + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/cache_symfony.yml'); + $kernel->addTestConfig(__DIR__.'/config/geo_plugin_fakeip_with_cache_fr.yml'); + }]); + $kernel->setClearCacheAfterShutdown(false); + $container = self::getContainer(); + + $geoPluginGeocoder = $container->get('bazinga_geocoder.provider.geoPlugin'); + $result = $geoPluginGeocoder->geocodeQuery(GeocodeQuery::create('::1')); + $country = $result->first()->getCountry()->getCode(); + self::assertEquals('FR', $country); + } +} diff --git a/tests/Functional/ProviderFactoryTest.php b/tests/Functional/ProviderFactoryTest.php new file mode 100644 index 00000000..e24dbde6 --- /dev/null +++ b/tests/Functional/ProviderFactoryTest.php @@ -0,0 +1,159 @@ +addTestBundle(BazingaGeocoderBundle::class); + $kernel->handleOptions($options); + + return $kernel; + } + + /** + * @param class-string $class + * @param list $serviceNames + * + * @dataProvider getProviders + */ + public function testProviderConfiguration(string $class, array $serviceNames): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) use ($class) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/provider/'.strtolower(substr($class, strrpos($class, '\\') + 1)).'.yml'); + }]); + + $container = self::getContainer(); + + foreach ($serviceNames as $name) { + self::assertTrue($container->has('bazinga_geocoder.provider.'.$name)); + $service = $container->get('bazinga_geocoder.provider.'.$name); + self::assertInstanceOf($class, NSA::getProperty($service, 'provider')); + } + } + + /** + * @return iterable}> + */ + public function getProviders(): iterable + { + yield [AlgoliaPlaces::class, ['empty', 'acme']]; + yield [ArcGISOnline::class, ['empty', 'acme']]; + yield [BingMaps::class, ['acme']]; + yield [Chain::class, ['acme']]; + yield [FreeGeoIp::class, ['empty', 'acme']]; + // yield [Geoip::class, ['empty']]; + yield [GeoIP2::class, ['acme']]; + if (class_exists(GeoIPs::class)) { + yield [GeoIPs::class, ['acme']]; + } + yield [Geonames::class, ['acme']]; + yield [GeoPlugin::class, ['empty']]; + yield [GoogleMaps::class, ['empty']]; + yield [GoogleMapsPlaces::class, ['acme']]; + yield [Here::class, ['acme']]; + yield [HostIp::class, ['empty']]; + yield [IpInfo::class, ['acme']]; + yield [IpInfoDb::class, ['empty', 'acme']]; + yield [Ipstack::class, ['acme']]; + yield [LocationIQ::class, ['acme']]; + yield [Mapbox::class, ['acme']]; + yield [MapQuest::class, ['acme']]; + if (class_exists(Mapzen::class)) { + yield [Mapzen::class, ['acme']]; + } + yield [MaxMind::class, ['acme']]; + yield [MaxMindBinary::class, ['acme']]; + yield [Nominatim::class, ['empty', 'acme']]; + yield [OpenCage::class, ['acme']]; + yield [PickPoint::class, ['acme']]; + yield [TomTom::class, ['acme']]; + yield [Yandex::class, ['empty', 'acme']]; + } + + /** + * @group legacy + */ + public function testProviderConfigurationWithDeprecatedHttplugClientOption(): void + { + self::bootKernel(['config' => static function (TestKernel $kernel) { + $kernel->addTestConfig(__DIR__.'/config/framework.yml'); + + if ($kernel::VERSION_ID >= 60000) { + $kernel->addTestConfig(__DIR__.'/config/framework_sf6.yml'); + } + + $kernel->addTestConfig(__DIR__.'/config/deprecated_httplug_client_option.yml'); + }]); + + $container = self::getContainer(); + + $this->expectDeprecation('Since willdurand/geocoder-bundle 5.19: The option "httplug_client" is deprecated, use "http_client" instead.'); + + self::assertTrue($container->has('bazinga_geocoder.provider.acme')); + $container->get('bazinga_geocoder.provider.acme'); + } +} diff --git a/tests/Functional/config/cache.yml b/tests/Functional/config/cache.yml new file mode 100644 index 00000000..e291985c --- /dev/null +++ b/tests/Functional/config/cache.yml @@ -0,0 +1,13 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory + cache_lifetime: 10 + cache_precision: 4 + cache: acme.cache + +services: + acme.cache: + class: Bazinga\GeocoderBundle\Tests\Functional\Helper\CacheHelper diff --git a/tests/Functional/config/cache_symfony.yml b/tests/Functional/config/cache_symfony.yml new file mode 100644 index 00000000..419c11d6 --- /dev/null +++ b/tests/Functional/config/cache_symfony.yml @@ -0,0 +1,12 @@ +framework: + cache: + app: cache.adapter.filesystem + system: cache.adapter.system + pools: + app.cache.geoPlugin: + adapter: cache.app + default_lifetime: 600 +services: + app.simple_cache: + class: Symfony\Component\Cache\Psr16Cache + arguments: ['@app.cache.geoPlugin'] \ No newline at end of file diff --git a/tests/Functional/config/cache_without_lifetime.yml b/tests/Functional/config/cache_without_lifetime.yml new file mode 100644 index 00000000..1c11bb00 --- /dev/null +++ b/tests/Functional/config/cache_without_lifetime.yml @@ -0,0 +1,12 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory + cache_precision: 4 + cache: acme.cache + +services: + acme.cache: + class: Bazinga\GeocoderBundle\Tests\Functional\Helper\CacheHelper diff --git a/tests/Functional/config/deprecated_httplug_client_option.yml b/tests/Functional/config/deprecated_httplug_client_option.yml new file mode 100644 index 00000000..c52ac75b --- /dev/null +++ b/tests/Functional/config/deprecated_httplug_client_option.yml @@ -0,0 +1,12 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory + options: + httplug_client: '@test_http_client' + +services: + test_http_client: + class: Http\Client\Curl\Client diff --git a/tests/Functional/config/framework.yml b/tests/Functional/config/framework.yml new file mode 100644 index 00000000..47b05a28 --- /dev/null +++ b/tests/Functional/config/framework.yml @@ -0,0 +1,10 @@ +framework: + http_method_override: false + secret: 6825c711ef47cfc1530d322b62adac3e2c43844c + session: + cookie_secure: auto + cookie_samesite: lax + handler_id: null + storage_factory_id: session.storage.factory.native + router: + utf8: true diff --git a/tests/Functional/config/framework_sf6.yml b/tests/Functional/config/framework_sf6.yml new file mode 100644 index 00000000..25f5a5b0 --- /dev/null +++ b/tests/Functional/config/framework_sf6.yml @@ -0,0 +1,5 @@ +framework: + annotations: false + handle_all_throwables: true + php_errors: + log: true diff --git a/tests/Functional/config/geo_plugin_fakeip_with_cache_cn.yml b/tests/Functional/config/geo_plugin_fakeip_with_cache_cn.yml new file mode 100644 index 00000000..6bf704cc --- /dev/null +++ b/tests/Functional/config/geo_plugin_fakeip_with_cache_cn.yml @@ -0,0 +1,14 @@ +# See the docs at https://github.com/geocoder-php/BazingaGeocoderBundle +bazinga_geocoder: + # The local IP (127.0.0.1) will be replaced by the fake_ip + # see https://github.com/geocoder-php/BazingaGeocoderBundle/blob/5.0.0/Resources/doc/index.md#fake-local-ip + fake_ip: + local_ip: ::1 + ip: 123.123.123.128 + # this ip is in china + providers: + geoPlugin: + factory: Bazinga\GeocoderBundle\ProviderFactory\GeoPluginFactory + cache: 'app.simple_cache' + cache_lifetime: 42 + cache_precision: ~ \ No newline at end of file diff --git a/tests/Functional/config/geo_plugin_fakeip_with_cache_fr.yml b/tests/Functional/config/geo_plugin_fakeip_with_cache_fr.yml new file mode 100644 index 00000000..f0b4b321 --- /dev/null +++ b/tests/Functional/config/geo_plugin_fakeip_with_cache_fr.yml @@ -0,0 +1,14 @@ +# See the docs at https://github.com/geocoder-php/BazingaGeocoderBundle +bazinga_geocoder: + # The local IP (127.0.0.1) will be replaced by the fake_ip + # see https://github.com/geocoder-php/BazingaGeocoderBundle/blob/5.0.0/Resources/doc/index.md#fake-local-ip + fake_ip: + local_ip: ::1 + ip: 87.98.128.10 + # this ip is in france + providers: + geoPlugin: + factory: Bazinga\GeocoderBundle\ProviderFactory\GeoPluginFactory + cache: 'app.simple_cache' + cache_lifetime: 42 + cache_precision: ~ \ No newline at end of file diff --git a/tests/Functional/config/listener.yml b/tests/Functional/config/listener.yml new file mode 100644 index 00000000..8fc0e6db --- /dev/null +++ b/tests/Functional/config/listener.yml @@ -0,0 +1,25 @@ +doctrine: + dbal: + default_connection: default + connections: + default: + driver: pdo_sqlite + path: '%kernel.cache_dir%/test.sqlite' + orm: + auto_generate_proxy_classes: true + validate_xml_mapping: true + report_fields_where_declared: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: false + +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\NominatimFactory + options: + http_client: '@Http\Mock\Client' + +services: + Http\Mock\Client: ~ diff --git a/tests/Functional/config/listener_php7.yml b/tests/Functional/config/listener_php7.yml new file mode 100644 index 00000000..da02623d --- /dev/null +++ b/tests/Functional/config/listener_php7.yml @@ -0,0 +1,34 @@ +doctrine: + orm: + mappings: + App: + is_bundle: false + type: annotation + dir: '%kernel.project_dir%/tests/Functional/Fixtures/Entity' + prefix: 'Bazinga\GeocoderBundle\Tests\Functional\Fixtures\Entity' + alias: App + +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\NominatimFactory + options: + http_client: '@Http\Mock\Client' + root_url: 'https://nominatim.openstreetmap.org' + user_agent: 'geocoder-php test_suite' + +services: + Bazinga\GeocoderBundle\Mapping\Driver\AnnotationDriver: + class: Bazinga\GeocoderBundle\Mapping\Driver\AnnotationDriver + arguments: + - '@annotations.reader' + + Bazinga\GeocoderBundle\Doctrine\ORM\GeocoderListener: + class: Bazinga\GeocoderBundle\Doctrine\ORM\GeocoderListener + arguments: + - '@bazinga_geocoder.provider.acme' + - '@Bazinga\GeocoderBundle\Mapping\Driver\AnnotationDriver' + tags: + - { name: doctrine.event_listener, event: onFlush } diff --git a/tests/Functional/config/listener_php8.yml b/tests/Functional/config/listener_php8.yml new file mode 100644 index 00000000..86732269 --- /dev/null +++ b/tests/Functional/config/listener_php8.yml @@ -0,0 +1,31 @@ +doctrine: + orm: + mappings: + App: + is_bundle: false + type: attribute + dir: '%kernel.project_dir%/tests/Functional/Fixtures/Entity' + prefix: 'Bazinga\GeocoderBundle\Tests\Functional\Fixtures\Entity' + alias: App + +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\NominatimFactory + options: + http_client: '@Http\Mock\Client' + root_url: 'https://nominatim.openstreetmap.org' + user_agent: 'geocoder-php test_suite' + +services: + Bazinga\GeocoderBundle\Mapping\Driver\AttributeDriver: ~ + + Bazinga\GeocoderBundle\Doctrine\ORM\GeocoderListener: + class: Bazinga\GeocoderBundle\Doctrine\ORM\GeocoderListener + arguments: + - '@bazinga_geocoder.provider.acme' + - '@Bazinga\GeocoderBundle\Mapping\Driver\AttributeDriver' + tags: + - { name: doctrine.event_listener, event: onFlush } diff --git a/tests/Functional/config/provider/algoliaplaces.yml b/tests/Functional/config/provider/algoliaplaces.yml new file mode 100644 index 00000000..e354b6d2 --- /dev/null +++ b/tests/Functional/config/provider/algoliaplaces.yml @@ -0,0 +1,11 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + empty: + factory: Bazinga\GeocoderBundle\ProviderFactory\AlgoliaFactory + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\AlgoliaFactory + options: + api_key: 'foo' + app_id: 'bar' diff --git a/tests/Functional/config/provider/arcgisonline.yml b/tests/Functional/config/provider/arcgisonline.yml new file mode 100644 index 00000000..9dc82140 --- /dev/null +++ b/tests/Functional/config/provider/arcgisonline.yml @@ -0,0 +1,10 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + empty: + factory: Bazinga\GeocoderBundle\ProviderFactory\ArcGISOnlineFactory + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\ArcGISOnlineFactory + options: + source_country: 'bar' diff --git a/tests/Functional/config/provider/bingmaps.yml b/tests/Functional/config/provider/bingmaps.yml new file mode 100644 index 00000000..6a042fe2 --- /dev/null +++ b/tests/Functional/config/provider/bingmaps.yml @@ -0,0 +1,8 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\BingMapsFactory + options: + api_key: 'bar' diff --git a/tests/Functional/config/provider/chain.yml b/tests/Functional/config/provider/chain.yml new file mode 100644 index 00000000..f74378ef --- /dev/null +++ b/tests/Functional/config/provider/chain.yml @@ -0,0 +1,8 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\ChainFactory + options: + services: ['foo'] diff --git a/tests/Functional/config/provider/freegeoip.yml b/tests/Functional/config/provider/freegeoip.yml new file mode 100644 index 00000000..3e2e859f --- /dev/null +++ b/tests/Functional/config/provider/freegeoip.yml @@ -0,0 +1,10 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + empty: + factory: Bazinga\GeocoderBundle\ProviderFactory\FreeGeoIpFactory + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\FreeGeoIpFactory + options: + base_url: 'foo' diff --git a/tests/Functional/config/provider/geoip.yml b/tests/Functional/config/provider/geoip.yml new file mode 100644 index 00000000..2ede9cd2 --- /dev/null +++ b/tests/Functional/config/provider/geoip.yml @@ -0,0 +1,6 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + empty: + factory: Bazinga\GeocoderBundle\ProviderFactory\GeoipFactory diff --git a/tests/Functional/config/provider/geoip2.yml b/tests/Functional/config/provider/geoip2.yml new file mode 100644 index 00000000..349fc74b --- /dev/null +++ b/tests/Functional/config/provider/geoip2.yml @@ -0,0 +1,11 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GeoIP2Factory + options: + provider: 'webservice' + user_id: 'foo' + license_key: 'bar' + diff --git a/tests/Functional/config/provider/geoips.yml b/tests/Functional/config/provider/geoips.yml new file mode 100644 index 00000000..d0ba8313 --- /dev/null +++ b/tests/Functional/config/provider/geoips.yml @@ -0,0 +1,8 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GeoIPsFactory + options: + api_key: 'foo' diff --git a/tests/Functional/config/provider/geonames.yml b/tests/Functional/config/provider/geonames.yml new file mode 100644 index 00000000..28418df2 --- /dev/null +++ b/tests/Functional/config/provider/geonames.yml @@ -0,0 +1,8 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GeonamesFactory + options: + username: 'foo' diff --git a/tests/Functional/config/provider/geoplugin.yml b/tests/Functional/config/provider/geoplugin.yml new file mode 100644 index 00000000..4aa8ff7a --- /dev/null +++ b/tests/Functional/config/provider/geoplugin.yml @@ -0,0 +1,6 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + empty: + factory: Bazinga\GeocoderBundle\ProviderFactory\GeoPluginFactory diff --git a/tests/Functional/config/provider/googlemaps.yml b/tests/Functional/config/provider/googlemaps.yml new file mode 100644 index 00000000..9a0e5d80 --- /dev/null +++ b/tests/Functional/config/provider/googlemaps.yml @@ -0,0 +1,11 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + empty: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory + options: + region: 'foo' + api_key: 'bar' diff --git a/tests/Functional/config/provider/googlemapsplaces.yml b/tests/Functional/config/provider/googlemapsplaces.yml new file mode 100644 index 00000000..2b83b619 --- /dev/null +++ b/tests/Functional/config/provider/googlemapsplaces.yml @@ -0,0 +1,8 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsPlacesFactory + options: + api_key: 'bar' diff --git a/tests/Functional/config/provider/here.yml b/tests/Functional/config/provider/here.yml new file mode 100644 index 00000000..a943e82b --- /dev/null +++ b/tests/Functional/config/provider/here.yml @@ -0,0 +1,9 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\HereFactory + options: + app_id: 'foo' + app_code: 'bar' diff --git a/tests/Functional/config/provider/hostip.yml b/tests/Functional/config/provider/hostip.yml new file mode 100644 index 00000000..9ccaecb6 --- /dev/null +++ b/tests/Functional/config/provider/hostip.yml @@ -0,0 +1,6 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + empty: + factory: Bazinga\GeocoderBundle\ProviderFactory\HostIpFactory diff --git a/tests/Functional/config/provider/ipinfo.yml b/tests/Functional/config/provider/ipinfo.yml new file mode 100644 index 00000000..e8c7c0a8 --- /dev/null +++ b/tests/Functional/config/provider/ipinfo.yml @@ -0,0 +1,6 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\IpInfoFactory diff --git a/tests/Functional/config/provider/ipinfodb.yml b/tests/Functional/config/provider/ipinfodb.yml new file mode 100644 index 00000000..aa0de471 --- /dev/null +++ b/tests/Functional/config/provider/ipinfodb.yml @@ -0,0 +1,13 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + empty: + factory: Bazinga\GeocoderBundle\ProviderFactory\IpInfoDbFactory + options: + api_key: 'bar' + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\IpInfoDbFactory + options: + precision: 'country' + api_key: 'bar' diff --git a/tests/Functional/config/provider/ipstack.yml b/tests/Functional/config/provider/ipstack.yml new file mode 100644 index 00000000..9835e5ab --- /dev/null +++ b/tests/Functional/config/provider/ipstack.yml @@ -0,0 +1,8 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\IpstackFactory + options: + api_key: 'foo' diff --git a/tests/Functional/config/provider/locationiq.yml b/tests/Functional/config/provider/locationiq.yml new file mode 100644 index 00000000..6fbfb367 --- /dev/null +++ b/tests/Functional/config/provider/locationiq.yml @@ -0,0 +1,8 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\LocationIQFactory + options: + api_key: 'foo' diff --git a/tests/Functional/config/provider/mapbox.yml b/tests/Functional/config/provider/mapbox.yml new file mode 100644 index 00000000..99980699 --- /dev/null +++ b/tests/Functional/config/provider/mapbox.yml @@ -0,0 +1,8 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\MapboxFactory + options: + api_key: 'bar' diff --git a/tests/Functional/config/provider/mapquest.yml b/tests/Functional/config/provider/mapquest.yml new file mode 100644 index 00000000..32a621c7 --- /dev/null +++ b/tests/Functional/config/provider/mapquest.yml @@ -0,0 +1,9 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\MapQuestFactory + options: + api_key: 'bar' + licensed: true diff --git a/tests/Functional/config/provider/mapzen.yml b/tests/Functional/config/provider/mapzen.yml new file mode 100644 index 00000000..2071e153 --- /dev/null +++ b/tests/Functional/config/provider/mapzen.yml @@ -0,0 +1,8 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\MapzenFactory + options: + api_key: 'bar' diff --git a/tests/Functional/config/provider/maxmind.yml b/tests/Functional/config/provider/maxmind.yml new file mode 100644 index 00000000..115302f0 --- /dev/null +++ b/tests/Functional/config/provider/maxmind.yml @@ -0,0 +1,9 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\MaxMindFactory + options: + api_key: 'bar' + endpoint: 'f' diff --git a/tests/Functional/config/provider/maxmindbinary.yml b/tests/Functional/config/provider/maxmindbinary.yml new file mode 100644 index 00000000..068994d0 --- /dev/null +++ b/tests/Functional/config/provider/maxmindbinary.yml @@ -0,0 +1,8 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\MaxMindBinaryFactory + options: + dat_file: '%kernel.project_dir%/tests/DependencyInjection/Fixtures/maxmind.dat' diff --git a/tests/Functional/config/provider/nominatim.yml b/tests/Functional/config/provider/nominatim.yml new file mode 100644 index 00000000..628b09aa --- /dev/null +++ b/tests/Functional/config/provider/nominatim.yml @@ -0,0 +1,11 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + empty: + factory: Bazinga\GeocoderBundle\ProviderFactory\NominatimFactory + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\NominatimFactory + options: + root_url: 'https://nominatim.openstreetmap.org' + user_agent: 'geocoder-php test_suite' diff --git a/tests/Functional/config/provider/opencage.yml b/tests/Functional/config/provider/opencage.yml new file mode 100644 index 00000000..de117fa1 --- /dev/null +++ b/tests/Functional/config/provider/opencage.yml @@ -0,0 +1,8 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\OpenCageFactory + options: + api_key: 'foo' diff --git a/tests/Functional/config/provider/pickpoint.yml b/tests/Functional/config/provider/pickpoint.yml new file mode 100644 index 00000000..83938009 --- /dev/null +++ b/tests/Functional/config/provider/pickpoint.yml @@ -0,0 +1,8 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\PickPointFactory + options: + api_key: 'foo' diff --git a/tests/Functional/config/provider/tomtom.yml b/tests/Functional/config/provider/tomtom.yml new file mode 100644 index 00000000..16fd7300 --- /dev/null +++ b/tests/Functional/config/provider/tomtom.yml @@ -0,0 +1,8 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\TomTomFactory + options: + api_key: 'foo' diff --git a/tests/Functional/config/provider/yandex.yml b/tests/Functional/config/provider/yandex.yml new file mode 100644 index 00000000..62c2370e --- /dev/null +++ b/tests/Functional/config/provider/yandex.yml @@ -0,0 +1,10 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + empty: + factory: Bazinga\GeocoderBundle\ProviderFactory\YandexFactory + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\YandexFactory + options: + toponym: 'city' diff --git a/tests/Functional/config/service_plugin.xml b/tests/Functional/config/service_plugin.xml new file mode 100644 index 00000000..67dbd807 --- /dev/null +++ b/tests/Functional/config/service_plugin.xml @@ -0,0 +1,18 @@ + + + + + + + acme.logger + + + + + + + + + + + diff --git a/tests/Functional/config/service_plugin.yml b/tests/Functional/config/service_plugin.yml new file mode 100644 index 00000000..07bcdffb --- /dev/null +++ b/tests/Functional/config/service_plugin.yml @@ -0,0 +1,35 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\NominatimFactory + plugins: + - acme.logger + - acme.2nd_logger + - { 'reference': { 'enabled': false, 'id': acme.3d_logger } } + - { 'reference': { 'enabled': true, 'id': acme.4th_logger } } + +services: + logger: + class: Psr\Log\NullLogger + + acme.logger: + class: Geocoder\Plugin\Plugin\LoggerPlugin + arguments: + - '@logger' + + acme.2nd_logger: + class: Geocoder\Plugin\Plugin\LoggerPlugin + arguments: + - '@logger' + + acme.3d_logger: + class: Geocoder\Plugin\Plugin\LoggerPlugin + arguments: + - '@logger' + + acme.4th_logger: + class: Geocoder\Plugin\Plugin\LoggerPlugin + arguments: + - '@logger' diff --git a/tests/Functional/config/simple.yml b/tests/Functional/config/simple.yml new file mode 100644 index 00000000..4398900f --- /dev/null +++ b/tests/Functional/config/simple.yml @@ -0,0 +1,6 @@ +bazinga_geocoder: + profiling: + enabled: false + providers: + acme: + factory: Bazinga\GeocoderBundle\ProviderFactory\GoogleMapsFactory diff --git a/tests/Mapping/Driver/AnnotationDriverTest.php b/tests/Mapping/Driver/AnnotationDriverTest.php new file mode 100644 index 00000000..d90de53c --- /dev/null +++ b/tests/Mapping/Driver/AnnotationDriverTest.php @@ -0,0 +1,56 @@ + + */ +final class AnnotationDriverTest extends TestCase +{ + private AnnotationDriver $driver; + + protected function setUp(): void + { + $this->driver = new AnnotationDriver(new AnnotationReader()); + } + + public function testLoadMetadata(): void + { + $obj = new Dummy(); + $metadata = $this->driver->loadMetadataFromObject($obj); + + self::assertInstanceOf(\ReflectionProperty::class, $metadata->addressProperty); + self::assertInstanceOf(\ReflectionProperty::class, $metadata->latitudeProperty); + self::assertInstanceOf(\ReflectionProperty::class, $metadata->longitudeProperty); + } + + public function testLoadMetadataFromWrongObject(): void + { + $this->expectException(MappingException::class); + $this->expectExceptionMessage('The class '.Dummy2::class.' is not geocodeable'); + + $this->driver->loadMetadataFromObject(new Dummy2()); + } + + public function testIsGeocodable(): void + { + self::assertTrue($this->driver->isGeocodeable(new Dummy())); + } +} diff --git a/tests/Mapping/Driver/AttributeDriverTest.php b/tests/Mapping/Driver/AttributeDriverTest.php new file mode 100644 index 00000000..0df14500 --- /dev/null +++ b/tests/Mapping/Driver/AttributeDriverTest.php @@ -0,0 +1,71 @@ + + */ +final class AttributeDriverTest extends TestCase +{ + private AttributeDriver $driver; + + public static function setUpBeforeClass(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped(sprintf('"%s" is only supported on PHP 8', AttributeDriver::class)); + } + } + + protected function setUp(): void + { + $this->driver = new AttributeDriver(); + } + + /** + * @requires PHP 8.0 + */ + public function testLoadMetadata(): void + { + $obj = new Dummy(); + $metadata = $this->driver->loadMetadataFromObject($obj); + + self::assertInstanceOf('ReflectionProperty', $metadata->addressProperty); + self::assertInstanceOf('ReflectionProperty', $metadata->latitudeProperty); + self::assertInstanceOf('ReflectionProperty', $metadata->longitudeProperty); + } + + /** + * @requires PHP 8.0 + */ + public function testLoadMetadataFromWrongObject(): void + { + $this->expectException(MappingException::class); + $this->expectExceptionMessage('The class '.Dummy2::class.' is not geocodeable'); + + $this->driver->loadMetadataFromObject(new Dummy2()); + } + + /** + * @requires PHP 8.0 + */ + public function testIsGeocodable(): void + { + self::assertTrue($this->driver->isGeocodeable(new Dummy())); + } +} diff --git a/tests/Mapping/Driver/Fixtures/Dummy.php b/tests/Mapping/Driver/Fixtures/Dummy.php new file mode 100644 index 00000000..79a7391d --- /dev/null +++ b/tests/Mapping/Driver/Fixtures/Dummy.php @@ -0,0 +1,43 @@ +handleQuery($query, function (Query $query) { return $query; }, function () {}); + + self::assertSame($query->getText(), '123.123.123.123'); + } + + /** + * @testWith [null] + * [""] + */ + public function testEmptyLocalIpQuery(?string $localIp): void + { + $fakeIpPlugin = new FakeIpPlugin($localIp, '123.123.123.123'); + $query = GeocodeQuery::create('124.124.124.124'); + + /** @var Query $query */ + $query = $fakeIpPlugin->handleQuery($query, function (Query $query) { return $query; }, function () {}); + + self::assertSame($query->getText(), '123.123.123.123'); + } + + public function testHandleQueryUsingFaker(): void + { + $fakeIpPlugin = new FakeIpPlugin('127.0.0.1', '192.168.1.1', true); + $query = GeocodeQuery::create('127.0.0.1'); + + /** @var Query $query */ + $query = $fakeIpPlugin->handleQuery($query, function (Query $query) { return $query; }, function () {}); + + self::assertNotSame($query->getText(), '192.168.1.1'); + } +} diff --git a/tests/Validator/Constraint/AddressValidatorTest.php b/tests/Validator/Constraint/AddressValidatorTest.php new file mode 100644 index 00000000..268aa4ab --- /dev/null +++ b/tests/Validator/Constraint/AddressValidatorTest.php @@ -0,0 +1,119 @@ +on($requestMatcher, function (RequestInterface $request) { + switch ((string) $request->getUri()) { + case 'https://nominatim.openstreetmap.org/search?format=jsonv2&q=Berlin%2C%20Germany&addressdetails=1&extratags=1&limit=5': + $stream = $this->createMock(StreamInterface::class); + $stream->expects(self::once()) + ->method('__toString') + ->willReturn('[{"place_id":159647018,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":62422,"lat":"52.5170365","lon":"13.3888599","category":"boundary","type":"administrative","place_rank":8,"importance":0.7875390282491362,"addresstype":"city","name":"Berlin","display_name":"Berlin, Deutschland","address":{"city":"Berlin","ISO3166-2-lvl4":"DE-BE","country":"Deutschland","country_code":"de"},"extratags":{"ele": "35", "email": "info@berlin.de", "place": "city", "capital": "yes", "website": "http://www.berlin.de", "de:place": "city", "ref:nuts": "DE3;DE30;DE300", "wikidata": "Q64", "wikipedia": "de:Berlin", "population": "3769962", "ref:LOCODE": "DEBER", "ref:nuts:1": "DE3", "ref:nuts:2": "DE30", "ref:nuts:3": "DE300", "state_code": "BE", "name:prefix": "Land und Kreisfreie Stadt", "linked_place": "city", "official_status": "Land", "contact:facebook": "http://www.facebook.com/Berlin", "name:prefix:city": "Kreisfreie Stadt", "openGeoDB:loc_id": "14356", "capital_ISO3166-1": "yes", "name:prefix:state": "Land", "source:population": "https://download.statistik-berlin-brandenburg.de/fa93e3bd19a2e885/a5ecfb2fff6a/SB_A01-05-00_2020h02_BE.pdf", "license_plate_code": "B", "official_status:de": "Land", "official_status:en": "State", "official_status:ru": "земля", "geographical_region": "Barnim;Berliner Urstromtal;Teltow;Nauener Platte", "blind:description:de": "Auf www.berlinfuerblinde.de gibt es einen kostenlosen Audioguide und weitere Informationen.", "de:regionalschluessel": "110000000000", "openGeoDB:postal_codes": "10178,10115,10117,10119,10179,10243,10245,10247,10249,10315,10317,10318,10319,10365,10367,10369,10405,10407,10409,10435,10437,10439,10551,10553,10555,10557,10559,10585,10587,10589,10623,10625,10627,10629,10707,10709,10711,10713,10715,10717,10719,10777,10", "report_problems:website": "https://ordnungsamt.berlin.de/", "TMC:cid_58:tabcd_1:Class": "Area", "openGeoDB:license_plate_code": "B", "TMC:cid_58:tabcd_1:LCLversion": "12.0", "openGeoDB:telephone_area_code": "030", "TMC:cid_58:tabcd_1:LocationCode": "266", "de:amtlicher_gemeindeschluessel": "11000000", "openGeoDB:community_identification_number": "11000000"},"boundingbox":["52.3382448","52.6755087","13.0883450","13.7611609"]}]'); + + $response = $this->createMock(ResponseInterface::class); + $response->expects(self::once()) + ->method('getStatusCode') + ->willReturn(200); + $response->expects(self::once()) + ->method('getBody') + ->willReturn($stream); + + return $response; + case 'https://nominatim.openstreetmap.org/search?format=jsonv2&q=Bifrost%2C%20Nine%20Realms&addressdetails=1&extratags=1&limit=5': + $stream = $this->createMock(StreamInterface::class); + $stream->expects(self::once()) + ->method('__toString') + ->willReturn('[]'); + + $response = $this->createMock(ResponseInterface::class); + $response->expects(self::once()) + ->method('getStatusCode') + ->willReturn(200); + $response->expects(self::once()) + ->method('getBody') + ->willReturn($stream); + + return $response; + } + + self::fail(sprintf('Unexpected http call "%s %s".', $request->getMethod(), (string) $request->getUri())); + }); + + $geocoder = Nominatim::withOpenStreetMapServer($httpClient, 'BazingaGeocoderBundle/Test'); + + return new AddressValidator($geocoder); + } + + public function testNullIsValid(): void + { + $this->validator->validate(null, new Address()); + + $this->assertNoViolation(); + } + + public function testEmptyStringIsValid(): void + { + $this->validator->validate('', new Address()); + + $this->assertNoViolation(); + } + + public function testExpectsStringCompatibleType(): void + { + $this->expectException(UnexpectedValueException::class); + + $this->validator->validate(new \stdClass(), new Address()); + } + + public function testValidAddress(): void + { + $this->validator->validate('Berlin, Germany', new Address()); + + $this->assertNoViolation(); + } + + public function testInvalidAddress(): void + { + $address = 'Bifrost, Nine Realms'; + + $constraint = new Address([ + 'message' => 'myMessage {{ address }}', + ]); + + $this->validator->validate($address, $constraint); + + $this->buildViolation('myMessage {{ address }}') + ->setParameter('{{ address }}', '"'.$address.'"') + ->setInvalidValue($address) + ->setCode(Address::INVALID_ADDRESS_ERROR) + ->assertRaised(); + } +} diff --git a/tests/baseline-ignore b/tests/baseline-ignore new file mode 100644 index 00000000..6642abb6 --- /dev/null +++ b/tests/baseline-ignore @@ -0,0 +1,85 @@ +%Bazinga\\GeocoderBundle\\ProviderFactory\\GeoIPsFactory is deprecated since 5.6, to be removed in 6.0% +%Bazinga\\GeocoderBundle\\ProviderFactory\\MapzenFactory is deprecated since 5.6, to be removed in 6.0% +%Since doctrine/doctrine-bundle 2.11: Not setting "enable_lazy_ghost_objects" to true is deprecated% +%Not configuring a schema manager factory is deprecated. Use Doctrine\\DBAL\\Schema\\DefaultSchemaManagerFactory which is going to be the default in DBAL 4% +%Doctrine\\DBAL\\Configuration::setSQLLogger is deprecated, use setMiddlewares\(\) and Logging\\Middleware instead% +%Accessing Doctrine\\Common\\Lexer\\Token properties via ArrayAccess is deprecated, use the value, type or position property instead% +%Doctrine\\DBAL\\Connection::getEventManager is deprecated% +%DebugStack is deprecated% +%In ORM 3.0, the AttributeDriver will report fields for the classes where they are declared% +%Class "Nyholm\\Psr7\\Factory\\HttplugFactory" is deprecated since version 1.8, use "Nyholm\\Psr7\\Factory\\Psr17Factory" instead% +%Not enabling lazy ghost objects is deprecated and will not be supported in Doctrine ORM 3.0% +%Since doctrine/doctrine-bundle 2.11: Not setting "doctrine.orm.enable_lazy_ghost_objects" to true is deprecated% +%Geocoder\\Http\\Provider\\AbstractHttpProvider::createRequest\(\): Implicitly marking parameter \$body as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\AdminLevel::__construct\(\): Implicitly marking parameter \$code as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\AdminLevelCollection::slice\(\): Implicitly marking parameter \$length as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\ProviderAggregator::getProvider\(\): Implicitly marking parameter \$currentProvider as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\AddressCollection::slice\(\): Implicitly marking parameter \$length as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Collection::slice\(\): Implicitly marking parameter \$length as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\BingMaps\\BingMaps::executeQuery\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\GoogleMaps\\GoogleMaps::__construct\(\): Implicitly marking parameter \$region as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\GoogleMaps\\GoogleMaps::__construct\(\): Implicitly marking parameter \$apiKey as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\GoogleMaps\\GoogleMaps::business\(\): Implicitly marking parameter \$privateKey as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\GoogleMaps\\GoogleMaps::business\(\): Implicitly marking parameter \$region as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\GoogleMaps\\GoogleMaps::business\(\): Implicitly marking parameter \$apiKey as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\GoogleMaps\\GoogleMaps::business\(\): Implicitly marking parameter \$channel as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\GoogleMaps\\GoogleMaps::buildQuery\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\GoogleMaps\\GoogleMaps::buildQuery\(\): Implicitly marking parameter \$region as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\GoogleMaps\\GoogleMaps::fetchUrl\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\GoogleMaps\\GoogleMaps::fetchUrl\(\): Implicitly marking parameter \$region as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Nominatim\\Nominatim::executeQuery\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Plugin\\Promise\\GeocoderFulfilledPromise::then\(\): Implicitly marking parameter \$onFulfilled as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Plugin\\Promise\\GeocoderFulfilledPromise::then\(\): Implicitly marking parameter \$onRejected as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Plugin\\Plugin\\CachePlugin::__construct\(\): Implicitly marking parameter \$lifetime as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Plugin\\Plugin\\CachePlugin::__construct\(\): Implicitly marking parameter \$precision as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\AddressBuilder::addAdminLevel\(\): Implicitly marking parameter \$code as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Nominatim\\Model\\NominatimAddress::withAttribution\(\): Implicitly marking parameter \$attribution as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Nominatim\\Model\\NominatimAddress::withClass\(\): Implicitly marking parameter \$category as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Nominatim\\Model\\NominatimAddress::withCategory\(\): Implicitly marking parameter \$category as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Nominatim\\Model\\NominatimAddress::withDisplayName\(\): Implicitly marking parameter \$displayName as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Nominatim\\Model\\NominatimAddress::withOSMId\(\): Implicitly marking parameter \$osmId as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Nominatim\\Model\\NominatimAddress::withOSMType\(\): Implicitly marking parameter \$osmType as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Nominatim\\Model\\NominatimAddress::withType\(\): Implicitly marking parameter \$type as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Nominatim\\Model\\NominatimAddress::withQuarter\(\): Implicitly marking parameter \$quarter as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Nominatim\\Model\\NominatimAddress::withDetails\(\): Implicitly marking parameter \$details as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Nominatim\\Model\\NominatimAddress::withTags\(\): Implicitly marking parameter \$tags as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Nominatim\\Model\\NominatimAddress::withNeighbourhood\(\): Implicitly marking parameter \$neighbourhood as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\AlgoliaPlaces\\AlgoliaPlaces::__construct\(\): Implicitly marking parameter \$apiKey as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\AlgoliaPlaces\\AlgoliaPlaces::__construct\(\): Implicitly marking parameter \$appId as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\AlgoliaPlaces\\AlgoliaPlaces::buildResult\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\AlgoliaPlaces\\AlgoliaPlaces::getResultAttribute\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\ArcGISOnline\\ArcGISOnline::__construct\(\): Implicitly marking parameter \$sourceCountry as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\ArcGISOnline\\ArcGISOnline::__construct\(\): Implicitly marking parameter \$token as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\ArcGISOnline\\ArcGISOnline::token\(\): Implicitly marking parameter \$sourceCountry as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Geonames\\Geonames::getCountryInfo\(\): Implicitly marking parameter \$country as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Geonames\\Geonames::getCountryInfo\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Geonames\\Geonames::executeQuery\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Here\\Here::__construct\(\): Implicitly marking parameter \$appId as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Here\\Here::__construct\(\): Implicitly marking parameter \$appCode as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\LocationIQ\\LocationIQ::__construct\(\): Implicitly marking parameter \$region as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\LocationIQ\\LocationIQ::executeQuery\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Mapbox\\Mapbox::__construct\(\): Implicitly marking parameter \$country as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Mapbox\\Mapbox::buildQuery\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Mapbox\\Mapbox::buildQuery\(\): Implicitly marking parameter \$country as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Mapbox\\Mapbox::fetchUrl\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Mapbox\\Mapbox::fetchUrl\(\): Implicitly marking parameter \$country as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\OpenCage\\OpenCage::executeQuery\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\PickPoint\\PickPoint::executeQuery\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Yandex\\Yandex::__construct\(\): Implicitly marking parameter \$toponym as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Yandex\\Yandex::__construct\(\): Implicitly marking parameter \$apiKey as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Provider\\Yandex\\Yandex::executeQuery\(\): Implicitly marking parameter \$locale as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Http\\Provider\\AbstractHttpProvider::__construct\(\): Implicitly marking parameter \$factory as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\Country::__construct\(\): Implicitly marking parameter \$name as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\Country::__construct\(\): Implicitly marking parameter \$code as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\Address::__construct\(\): Implicitly marking parameter \$coordinates as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\Address::__construct\(\): Implicitly marking parameter \$bounds as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\Address::__construct\(\): Implicitly marking parameter \$streetNumber as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\Address::__construct\(\): Implicitly marking parameter \$streetName as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\Address::__construct\(\): Implicitly marking parameter \$postalCode as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\Address::__construct\(\): Implicitly marking parameter \$locality as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\Address::__construct\(\): Implicitly marking parameter \$subLocality as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\Address::__construct\(\): Implicitly marking parameter \$country as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\Model\\Address::__construct\(\): Implicitly marking parameter \$timezone as nullable is deprecated, the explicit nullable type must be used instead% +%Geocoder\\ProviderAggregator::__construct\(\): Implicitly marking parameter \$decider as nullable is deprecated, the explicit nullable type must be used instead% +%Since symfony/var-exporter 7.3: The "Symfony\\Component\\VarExporter\\LazyGhostTrait" trait is deprecated, use native lazy objects instead.% +%Method "Http\\Promise\\Promise::wait\(\)" might add " Resolved value, null if \$unwrap is set to false" as a native return type declaration in the future. Do the same in implementation "Geocoder\\Plugin\\Promise\\GeocoderFulfilledPromise" now to avoid errors or add an explicit @return annotation to suppress this message.% diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..d0cb7088 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,21 @@ +