diff --git a/DependencyInjection/BazingaGeocoderExtension.php b/DependencyInjection/BazingaGeocoderExtension.php index 7ab721d0..87781321 100644 --- a/DependencyInjection/BazingaGeocoderExtension.php +++ b/DependencyInjection/BazingaGeocoderExtension.php @@ -53,6 +53,10 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('profiling.yml'); } + if (array_key_exists('DoctrineBundle', $container->getParameter('kernel.bundles'))) { + $loader->load('doctrine.yml'); + } + if ($config['fake_ip']['enabled']) { $definition = $container->getDefinition(FakeIpPlugin::class); $definition->replaceArgument(0, $config['fake_ip']['local_ip']); diff --git a/Doctrine/ORM/GeocodeEntityListener.php b/Doctrine/ORM/GeocodeEntityListener.php new file mode 100644 index 00000000..807c8c23 --- /dev/null +++ b/Doctrine/ORM/GeocodeEntityListener.php @@ -0,0 +1,141 @@ + + * @author Pierre du Plessis + */ +class GeocodeEntityListener implements EventSubscriber +{ + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ServiceLocator + */ + private $providerLocator; + + public function __construct(ServiceLocator $providerLocator, DriverInterface $driver) + { + $this->driver = $driver; + $this->providerLocator = $providerLocator; + } + + /** + * {@inheritdoc} + */ + public function getSubscribedEvents() + { + return [ + Events::onFlush, + ]; + } + + public function onFlush(OnFlushEventArgs $args) + { + $em = $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 + */ + private function geocodeEntity(ClassMetadata $metadata, $entity) + { + if (null !== $metadata->addressGetter) { + $address = $metadata->addressGetter->invoke($entity); + } else { + $address = $metadata->addressProperty->getValue($entity); + } + + if (empty($address)) { + return; + } + + $serviceId = sprintf('bazinga_geocoder.provider.%s', $metadata->provider); + + if (!$this->providerLocator->has($serviceId)) { + throw new \RuntimeException(sprintf('The provider "%s" is invalid for object "%s".', $metadata->provider, get_class($entity))); + } + + $results = $this->providerLocator->get($serviceId)->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/Doctrine/ORM/GeocoderListener.php b/Doctrine/ORM/GeocoderListener.php index 3e8b4d6b..f3b440e7 100644 --- a/Doctrine/ORM/GeocoderListener.php +++ b/Doctrine/ORM/GeocoderListener.php @@ -12,123 +12,25 @@ namespace Bazinga\GeocoderBundle\Doctrine\ORM; -use Bazinga\GeocoderBundle\Mapping\ClassMetadata; use Bazinga\GeocoderBundle\Mapping\Driver\DriverInterface; -use Doctrine\Common\EventSubscriber; -use Doctrine\ORM\Event\OnFlushEventArgs; -use Doctrine\ORM\Events; -use Doctrine\ORM\UnitOfWork; use Geocoder\Provider\Provider; -use Geocoder\Query\GeocodeQuery; +use Symfony\Component\DependencyInjection\ServiceLocator; /** * @author Markus Bachmann */ -class GeocoderListener implements EventSubscriber +class GeocoderListener extends GeocodeEntityListener { - /** - * @var DriverInterface - */ - private $driver; - - /** - * @var Provider - */ - private $geocoder; - public function __construct(Provider $geocoder, DriverInterface $driver) { - $this->driver = $driver; - $this->geocoder = $geocoder; - } - - /** - * {@inheritdoc} - */ - public function getSubscribedEvents() - { - return [ - Events::onFlush, - ]; - } - - public function onFlush(OnFlushEventArgs $args) - { - $em = $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 - */ - private function geocodeEntity(ClassMetadata $metadata, $entity) - { - if (null !== $metadata->addressGetter) { - $address = $metadata->addressGetter->invoke($entity); - } else { - $address = $metadata->addressProperty->getValue($entity); - } - - if (empty($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; - } + @trigger_error(sprintf('The class "%s" is deprecated and will be removed from a future version. Please remove it from your service definition.', self::class)); - $changeSet = $unitOfWork->getEntityChangeSet($entity); + $locator = new ServiceLocator([ + 'bazinga_geocoder.provider.' => function () use ($geocoder) { + return $geocoder; + }, + ]); - return isset($changeSet[$metadata->addressProperty->getName()]); + parent::__construct($locator, $driver); } } diff --git a/Mapping/Annotations/Geocodeable.php b/Mapping/Annotations/Geocodeable.php index fce6311e..cd38b0f3 100644 --- a/Mapping/Annotations/Geocodeable.php +++ b/Mapping/Annotations/Geocodeable.php @@ -20,4 +20,13 @@ */ class Geocodeable { + /** + * @var string + */ + public $provider = null; + + public function __construct(array $options = [], string $provider = null) + { + $this->provider = $options['provider'] ?? $provider; + } } diff --git a/Mapping/ClassMetadata.php b/Mapping/ClassMetadata.php index 891bffb2..df8f8424 100644 --- a/Mapping/ClassMetadata.php +++ b/Mapping/ClassMetadata.php @@ -36,4 +36,9 @@ class ClassMetadata * @var \ReflectionMethod */ public $addressGetter; + + /** + * @var string|null + */ + public $provider = null; } diff --git a/Mapping/Driver/AnnotationDriver.php b/Mapping/Driver/AnnotationDriver.php index 8343f8d8..6c1fde51 100644 --- a/Mapping/Driver/AnnotationDriver.php +++ b/Mapping/Driver/AnnotationDriver.php @@ -46,6 +46,7 @@ public function loadMetadataFromObject($object) } $metadata = new ClassMetadata(); + $metadata->provider = $annotation->provider; foreach ($reflection->getProperties() as $property) { foreach ($this->reader->getPropertyAnnotations($property) as $annotation) { diff --git a/Mapping/Driver/AttributeDriver.php b/Mapping/Driver/AttributeDriver.php index 2b1cbaaf..f44ecae8 100644 --- a/Mapping/Driver/AttributeDriver.php +++ b/Mapping/Driver/AttributeDriver.php @@ -51,6 +51,7 @@ public function loadMetadataFromObject($object): ClassMetadata } $metadata = new ClassMetadata(); + $metadata->provider = $attributes[0]->newInstance()->provider; foreach ($reflection->getProperties() as $property) { foreach ($property->getAttributes() as $attribute) { diff --git a/Mapping/Driver/ChainDriver.php b/Mapping/Driver/ChainDriver.php new file mode 100644 index 00000000..04bcd528 --- /dev/null +++ b/Mapping/Driver/ChainDriver.php @@ -0,0 +1,52 @@ + + */ +class ChainDriver implements DriverInterface +{ + private $drivers; + + public function __construct(iterable $drivers) + { + $this->drivers = $drivers; + } + + public function isGeocodeable($object): bool + { + foreach ($this->drivers as $driver) { + if ($driver->isGeocodeable($object)) { + return true; + } + } + + return false; + } + + public function loadMetadataFromObject($object) + { + foreach ($this->drivers as $driver) { + try { + return $driver->loadMetadataFromObject($object); + } catch (MappingException $exception) { + continue; + } + } + + throw new MappingException(sprintf('The class %s is not geocodeable', get_class($object))); + } +} diff --git a/Mapping/Driver/DriverInterface.php b/Mapping/Driver/DriverInterface.php index 54ebc9df..0f61d862 100644 --- a/Mapping/Driver/DriverInterface.php +++ b/Mapping/Driver/DriverInterface.php @@ -12,9 +12,14 @@ namespace Bazinga\GeocoderBundle\Mapping\Driver; +use Bazinga\GeocoderBundle\Mapping\Exception\MappingException; + interface DriverInterface { public function isGeocodeable($object): bool; + /** + * @throws MappingException + */ public function loadMetadataFromObject($object); } diff --git a/Resources/config/doctrine.yml b/Resources/config/doctrine.yml new file mode 100644 index 00000000..815565c4 --- /dev/null +++ b/Resources/config/doctrine.yml @@ -0,0 +1,15 @@ +services: + Bazinga\GeocoderBundle\Mapping\Driver\AnnotationDriver: + class: Bazinga\GeocoderBundle\Mapping\Driver\AnnotationDriver + arguments: + - '@annotations.reader' + tags: + - bazinga_geocoder.metadata.driver + + Bazinga\GeocoderBundle\Doctrine\ORM\GeocodeEntityListener: + class: Bazinga\GeocoderBundle\Doctrine\ORM\GeocodeEntityListener + arguments: + - !tagged_locator 'bazinga_geocoder.provider' + - '@Bazinga\GeocoderBundle\Mapping\Driver\DriverInterface' + tags: + - doctrine.event_subscriber diff --git a/Resources/config/services.yml b/Resources/config/services.yml index c4cdb688..a19971da 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -38,3 +38,15 @@ services: geocoder: alias: "Geocoder\\ProviderAggregator" + + Bazinga\GeocoderBundle\Mapping\Driver\ChainDriver: + class: Bazinga\GeocoderBundle\Mapping\Driver\ChainDriver + arguments: + - !tagged_iterator bazinga_geocoder.metadata.driver + + Bazinga\GeocoderBundle\Mapping\Driver\AttributeDriver: + class: Bazinga\GeocoderBundle\Mapping\Driver\AttributeDriver + tags: + - { name: bazinga_geocoder.metadata.driver } + + Bazinga\GeocoderBundle\Mapping\Driver\DriverInterface: '@Bazinga\GeocoderBundle\Mapping\Driver\ChainDriver' diff --git a/Resources/doc/doctrine.md b/Resources/doc/doctrine.md index 06580ba1..e985edc4 100644 --- a/Resources/doc/doctrine.md +++ b/Resources/doc/doctrine.md @@ -65,41 +65,7 @@ class User } ``` -Secondly, register the Doctrine event listener and its dependencies in your `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: - - doctrine.event_subscriber -``` - -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: +If you are using PHP 8, then you can use [Attributes](https://www.php.net/manual/en/language.attributes.overview.php) in your entity: ```php @@ -119,17 +85,15 @@ class User } ``` -Then update your service configuration to register the `AttributeDriver`: +It is done! +Now you can use it: -```yaml - Bazinga\GeocoderBundle\Mapping\Driver\AttributeDriver: - class: Bazinga\GeocoderBundle\Mapping\Driver\AttributeDriver +```php +$user = new User(); +$user->setAddress('Brandenburger Tor, Pariser Platz, Berlin'); +$em->persist($event); +$em->flush(); - Bazinga\GeocoderBundle\Doctrine\ORM\GeocoderListener: - class: Bazinga\GeocoderBundle\Doctrine\ORM\GeocoderListener - arguments: - - '@bazinga_geocoder.provider.acme' - - '@Bazinga\GeocoderBundle\Mapping\Driver\AttributeDriver' - tags: - - doctrine.event_subscriber +echo $user->getLatitude(); // will output 52.516325 +echo $user->getLongitude(); // will output 13.377264 ```