From 0c30a7029e1a4375a28667d5bc1aa8bc51037f12 Mon Sep 17 00:00:00 2001 From: Aymeric Mayeux Date: Tue, 3 Feb 2026 14:06:19 +0100 Subject: [PATCH 1/2] feat: Adds Doctrine attributes to map common Location properties Adds additionnal attributes to be able to automatically map them to Doctrine entities. --- src/Doctrine/ORM/GeocodeEntityListener.php | 10 ++++ src/Mapping/Attributes/Country.php | 16 ++++++ src/Mapping/Attributes/East.php | 16 ++++++ src/Mapping/Attributes/Locality.php | 16 ++++++ src/Mapping/Attributes/North.php | 16 ++++++ src/Mapping/Attributes/PostalCode.php | 16 ++++++ src/Mapping/Attributes/South.php | 16 ++++++ src/Mapping/Attributes/StreetName.php | 16 ++++++ src/Mapping/Attributes/StreetNumber.php | 16 ++++++ src/Mapping/Attributes/SubLocality.php | 16 ++++++ src/Mapping/Attributes/West.php | 16 ++++++ src/Mapping/ClassMetadata.php | 10 ++++ src/Mapping/Driver/AttributeDriver.php | 24 ++++++--- .../Fixtures/Entity/DummyWithProperty.php | 50 +++++++++++++++++++ .../Functional/GeocodeEntityListenerTest.php | 45 +++++++++++++---- tests/Mapping/Driver/AttributeDriverTest.php | 20 ++++++++ tests/Mapping/Driver/Fixtures/Dummy.php | 40 +++++++++++++++ 17 files changed, 342 insertions(+), 17 deletions(-) create mode 100644 src/Mapping/Attributes/Country.php create mode 100644 src/Mapping/Attributes/East.php create mode 100644 src/Mapping/Attributes/Locality.php create mode 100644 src/Mapping/Attributes/North.php create mode 100644 src/Mapping/Attributes/PostalCode.php create mode 100644 src/Mapping/Attributes/South.php create mode 100644 src/Mapping/Attributes/StreetName.php create mode 100644 src/Mapping/Attributes/StreetNumber.php create mode 100644 src/Mapping/Attributes/SubLocality.php create mode 100644 src/Mapping/Attributes/West.php diff --git a/src/Doctrine/ORM/GeocodeEntityListener.php b/src/Doctrine/ORM/GeocodeEntityListener.php index 239ae3da..c2d10ba8 100644 --- a/src/Doctrine/ORM/GeocodeEntityListener.php +++ b/src/Doctrine/ORM/GeocodeEntityListener.php @@ -107,6 +107,16 @@ private function geocodeEntity(ClassMetadata $metadata, object $entity): void $result = $results->first(); $metadata->latitudeProperty?->setValue($entity, $result->getCoordinates()->getLatitude()); $metadata->longitudeProperty?->setValue($entity, $result->getCoordinates()->getLongitude()); + $metadata->northProperty?->setValue($entity, $result->getBounds()?->getNorth()); + $metadata->southProperty?->setValue($entity, $result->getBounds()?->getSouth()); + $metadata->eastProperty?->setValue($entity, $result->getBounds()?->getEast()); + $metadata->westProperty?->setValue($entity, $result->getBounds()?->getWest()); + $metadata->streetNumberProperty?->setValue($entity, $result->getStreetNumber()); + $metadata->streetNameProperty?->setValue($entity, $result->getStreetName()); + $metadata->localityProperty?->setValue($entity, $result->getLocality()); + $metadata->postalCodeProperty?->setValue($entity, $result->getPostalCode()); + $metadata->subLocalityProperty?->setValue($entity, $result->getSubLocality()); + $metadata->countryProperty?->setValue($entity, $result->getCountry()); } } diff --git a/src/Mapping/Attributes/Country.php b/src/Mapping/Attributes/Country.php new file mode 100644 index 00000000..dfb6c437 --- /dev/null +++ b/src/Mapping/Attributes/Country.php @@ -0,0 +1,16 @@ + 'latitudeProperty', + Attributes\Longitude::class => 'longitudeProperty', + Attributes\Address::class => 'addressProperty', + Attributes\North::class => 'northProperty', + Attributes\South::class => 'southProperty', + Attributes\East::class => 'eastProperty', + Attributes\West::class => 'westProperty', + Attributes\StreetNumber::class => 'streetNumberProperty', + Attributes\StreetName::class => 'streetNameProperty', + Attributes\Locality::class => 'localityProperty', + Attributes\PostalCode::class => 'postalCodeProperty', + Attributes\SubLocality::class => 'subLocalityProperty', + Attributes\Country::class => 'countryProperty', + ]; + public function isGeocodeable(object $object): bool { $reflection = self::getReflection($object); @@ -47,12 +63,8 @@ public function loadMetadataFromObject(object $object): ClassMetadata foreach ($reflection->getProperties() as $property) { foreach ($property->getAttributes() as $attribute) { - if (Attributes\Latitude::class === $attribute->getName()) { - $args['latitudeProperty'] = $property; - } elseif (Attributes\Longitude::class === $attribute->getName()) { - $args['longitudeProperty'] = $property; - } elseif (Attributes\Address::class === $attribute->getName()) { - $args['addressProperty'] = $property; + if (isset(self::PROPERTY_MATRIX[$attribute->getName()])) { + $args[self::PROPERTY_MATRIX[$attribute->getName()]] = $property; } } } diff --git a/tests/Functional/Fixtures/Entity/DummyWithProperty.php b/tests/Functional/Fixtures/Entity/DummyWithProperty.php index df222c93..c4201bc3 100644 --- a/tests/Functional/Fixtures/Entity/DummyWithProperty.php +++ b/tests/Functional/Fixtures/Entity/DummyWithProperty.php @@ -13,9 +13,19 @@ namespace Bazinga\GeocoderBundle\Tests\Functional\Fixtures\Entity; use Bazinga\GeocoderBundle\Mapping\Attributes\Address; +use Bazinga\GeocoderBundle\Mapping\Attributes\Country; +use Bazinga\GeocoderBundle\Mapping\Attributes\East; use Bazinga\GeocoderBundle\Mapping\Attributes\Geocodeable; use Bazinga\GeocoderBundle\Mapping\Attributes\Latitude; +use Bazinga\GeocoderBundle\Mapping\Attributes\Locality; use Bazinga\GeocoderBundle\Mapping\Attributes\Longitude; +use Bazinga\GeocoderBundle\Mapping\Attributes\North; +use Bazinga\GeocoderBundle\Mapping\Attributes\PostalCode; +use Bazinga\GeocoderBundle\Mapping\Attributes\South; +use Bazinga\GeocoderBundle\Mapping\Attributes\StreetName; +use Bazinga\GeocoderBundle\Mapping\Attributes\StreetNumber; +use Bazinga\GeocoderBundle\Mapping\Attributes\SubLocality; +use Bazinga\GeocoderBundle\Mapping\Attributes\West; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; @@ -42,4 +52,44 @@ class DummyWithProperty #[Column(type: Types::STRING)] #[Address] public ?string $address = null; + + #[Column(type: Types::FLOAT, nullable: true)] + #[North] + public ?float $north = null; + + #[Column(type: Types::FLOAT, nullable: true)] + #[South] + public ?float $south = null; + + #[Column(type: Types::FLOAT, nullable: true)] + #[East] + public ?float $east = null; + + #[Column(type: Types::FLOAT, nullable: true)] + #[West] + public ?float $west = null; + + #[Column(type: Types::STRING, nullable: true)] + #[StreetNumber] + public int|string|null $streetNumber = null; + + #[Column(type: Types::STRING, nullable: true)] + #[StreetName] + public ?string $streetName = null; + + #[Column(type: Types::STRING, nullable: true)] + #[Locality] + public ?string $locality = null; + + #[Column(type: Types::STRING, nullable: true)] + #[PostalCode] + public ?string $postalCode = null; + + #[Column(type: Types::STRING, nullable: true)] + #[SubLocality] + public ?string $subLocality = null; + + #[Column(type: Types::STRING, nullable: true)] + #[Country] + public ?string $country = null; } diff --git a/tests/Functional/GeocodeEntityListenerTest.php b/tests/Functional/GeocodeEntityListenerTest.php index e55a7564..166dfda6 100644 --- a/tests/Functional/GeocodeEntityListenerTest.php +++ b/tests/Functional/GeocodeEntityListenerTest.php @@ -37,6 +37,9 @@ */ final class GeocodeEntityListenerTest extends KernelTestCase { + private const QUERY_BERLIN = 'Brandenburger Tor Berlin, Germany'; + private const QUERY_PARIS = 'Tour Eiffel Paris, France'; + protected function tearDown(): void { /** @var EntityManagerInterface $em */ @@ -118,7 +121,7 @@ public function testPersistForProperty(): void $container->set('http_client', self::createHttpClientForBerlinQuery()); $dummy = new DummyWithProperty(); - $dummy->address = 'Berlin, Germany'; + $dummy->address = self::QUERY_BERLIN; /** @var EntityManagerInterface $em */ $em = $container->get('doctrine.orm.entity_manager'); @@ -131,15 +134,35 @@ public function testPersistForProperty(): void self::assertNotNull($dummy->latitude); self::assertNotNull($dummy->longitude); + self::assertNotNull($dummy->north); + self::assertNotNull($dummy->south); + self::assertNotNull($dummy->east); + self::assertNotNull($dummy->west); + self::assertNotNull($dummy->streetNumber); + self::assertNotNull($dummy->streetName); + self::assertNotNull($dummy->locality); + self::assertNotNull($dummy->postalCode); + self::assertNotNull($dummy->subLocality); + self::assertNotNull($dummy->country); $clone = clone $dummy; - $dummy->address = 'Paris, France'; + $dummy->address = self::QUERY_PARIS; $em->persist($dummy); $em->flush(); self::assertNotEquals($clone->latitude, $dummy->latitude); self::assertNotEquals($clone->longitude, $dummy->longitude); + self::assertNotEquals($clone->north, $dummy->north); + self::assertNotEquals($clone->south, $dummy->south); + self::assertNotEquals($clone->east, $dummy->east); + self::assertNotEquals($clone->west, $dummy->west); + self::assertNotEquals($clone->streetNumber, $dummy->streetNumber); + self::assertNotEquals($clone->streetName, $dummy->streetName); + self::assertNotEquals($clone->locality, $dummy->locality); + self::assertNotEquals($clone->postalCode, $dummy->postalCode); + self::assertNotEquals($clone->subLocality, $dummy->subLocality); + self::assertNotEquals($clone->country, $dummy->country); } public function testPersistForGetter(): void @@ -160,7 +183,7 @@ public function testPersistForGetter(): void $tool->createSchema($em->getMetadataFactory()->getAllMetadata()); $dummy = new DummyWithGetter(); - $dummy->setAddress('Berlin, Germany'); + $dummy->setAddress(self::QUERY_BERLIN); $em->persist($dummy); $em->flush(); @@ -169,7 +192,7 @@ public function testPersistForGetter(): void self::assertNotNull($dummy->getLongitude()); $clone = clone $dummy; - $dummy->setAddress('Paris, France'); + $dummy->setAddress(self::QUERY_PARIS); $em->persist($dummy); $em->flush(); @@ -195,7 +218,7 @@ public function testPersistForStringableGetter(): void $tool = new SchemaTool($em); $tool->createSchema($em->getMetadataFactory()->getAllMetadata()); - $dummy = new DummyWithStringableGetter(new StringableAddress('Berlin, Germany')); + $dummy = new DummyWithStringableGetter(new StringableAddress(self::QUERY_BERLIN)); $em->persist($dummy); $em->flush(); @@ -204,7 +227,7 @@ public function testPersistForStringableGetter(): void self::assertNotNull($dummy->longitude); $clone = clone $dummy; - $dummy->setAddress(new StringableAddress('Paris, France')); + $dummy->setAddress(new StringableAddress(self::QUERY_PARIS)); $em->persist($dummy); $em->flush(); @@ -233,7 +256,7 @@ public function testPersistForInvalidGetter(): void $tool->createSchema($em->getMetadataFactory()->getAllMetadata()); $dummy = new DummyWithInvalidGetter(); - $dummy->setAddress('Berlin, Germany'); + $dummy->setAddress(self::QUERY_BERLIN); $em->persist($dummy); @@ -310,16 +333,16 @@ public function testDoesNotGeocodeIfAddressNotChanged(): void private static function createHttpClientForBerlinQuery(): MockHttpClient { return new MockHttpClient(static function (string $method, string $url): MockResponse { - if ('https://nominatim.openstreetmap.org/search?format=jsonv2&q=Berlin%2C%20Germany&addressdetails=1&extratags=1&limit=5' === $url) { + if ('https://nominatim.openstreetmap.org/search?format=jsonv2&q=Brandenburger%20Tor%20Berlin%2C%20Germany&addressdetails=1&extratags=1&limit=5' === $url) { self::assertSame('GET', $method); - return new MockResponse('[{"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_headers' => ['content-type' => 'application/json']]); + return new MockResponse('[{"place_id":134111822,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"way","osm_id":518071791,"lat":"52.5162699","lon":"13.3777034","category":"historic","type":"monument","place_rank":30,"importance":0.5504719991549523,"addresstype":"historic","name":"Porte de Brandebourg","display_name":"Porte de Brandebourg, 1, Pariser Platz, Friedrich-Wilhelm-Stadt, Mitte, Berlin, 10117, Allemagne","address":{"historic":"Porte de Brandebourg","house_number":"1","road":"Pariser Platz","quarter":"Friedrich-Wilhelm-Stadt","suburb":"Mitte","borough":"Mitte","city":"Berlin","ISO3166-2-lvl4":"DE-BE","postcode":"10117","country":"Allemagne","country_code":"de"},"extratags":{"image": "http://upload.wikimedia.org/wikipedia/commons/7/71/Brandenburger_Tor_2005_006.JPG", "height": "26", "image:0": "https://photos.app.goo.gl/58KgcLTZ3qNYYhjc8", "ref:lda": "09065019", "tourism": "attraction", "building": "gate", "heritage": "4", "landmark": "20", "wikidata": "Q82425", "architect": "Carl Gotthard Langhans", "direction": "W", "wikipedia": "de:Brandenburger Tor", "start_date": "1788..1791", "wheelchair": "yes", "lda:criteria": "Baudenkmal", "opening_hours": "24/7", "heritage:website": "https://denkmaldatenbank.berlin.de/daobj.php?obj_dok_nr=09065019", "heritage:operator": "lda", "wikimedia_commons": "Category:Brandenburg Gate", "architect:wikidata": "Q313181", "toilets:wheelchair": "no", "architect:wikipedia": "de:Carl Gotthard Langhans", "heritage:description": "Baudenkmal", "construction:start_date": "1788"},"boundingbox":["52.5161170","52.5164328","13.3775798","13.3778251"]},{"place_id":133613781,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"node","osm_id":3862767512,"lat":"52.5166047","lon":"13.3809897","category":"railway","type":"station","place_rank":30,"importance":0.42779598793275175,"addresstype":"railway","name":"Brandenburger Tor","display_name":"Brandenburger Tor, Unter den Linden, Friedrich-Wilhelm-Stadt, Mitte, Berlin, 10117, Allemagne","address":{"railway":"Brandenburger Tor","road":"Unter den Linden","quarter":"Friedrich-Wilhelm-Stadt","suburb":"Mitte","borough":"Mitte","city":"Berlin","ISO3166-2-lvl4":"DE-BE","postcode":"10117","country":"Allemagne","country_code":"de"},"extratags":{"level": "-3", "subway": "yes", "station": "subway", "wikidata": "Q477185", "wikipedia": "de:Bahnhof Berlin Brandenburger Tor", "start_date": "2009-08-09", "wheelchair": "yes", "public_transport": "station"},"boundingbox":["52.5116047","52.5216047","13.3759897","13.3859897"]},{"place_id":134207485,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"node","osm_id":30353086,"lat":"52.5164415","lon":"13.3818418","category":"railway","type":"halt","place_rank":30,"importance":0.42779598793275175,"addresstype":"railway","name":"Brandenburger Tor","display_name":"Brandenburger Tor, Unter den Linden, Friedrich-Wilhelm-Stadt, Mitte, Berlin, 10117, Allemagne","address":{"railway":"Brandenburger Tor","road":"Unter den Linden","quarter":"Friedrich-Wilhelm-Stadt","suburb":"Mitte","borough":"Mitte","city":"Berlin","ISO3166-2-lvl4":"DE-BE","postcode":"10117","country":"Allemagne","country_code":"de"},"extratags":{"network": "Verkehrsverbund Berlin-Brandenburg", "station": "light_rail", "uic_ref": "8089044", "operator": "DB Netz AG", "wikidata": "Q477185", "wikipedia": "de:Bahnhof Berlin Brandenburger Tor", "light_rail": "yes", "wheelchair": "no", "railway:ref": "BTOR", "contact:phone": "+49 30 297 43333", "network:short": "VBB", "contact:website": "http://www.s-bahn-berlin.de/fahrplanundnetz/bahnhof/brandenburger-tor/153", "public_transport": "station", "railway:station_category": "4"},"boundingbox":["52.5163915","52.5164915","13.3817918","13.3818918"]}]', ['response_headers' => ['content-type' => 'application/json']]); } - if ('https://nominatim.openstreetmap.org/search?format=jsonv2&q=Paris%2C%20France&addressdetails=1&extratags=1&limit=5' === $url) { + if ('https://nominatim.openstreetmap.org/search?format=jsonv2&q=Tour%20Eiffel%20Paris%2C%20France&addressdetails=1&extratags=1&limit=5' === $url) { self::assertSame('GET', $method); - return new MockResponse('[{"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_headers' => ['content-type' => 'application/json']]); + return new MockResponse('[{"place_id":89233259,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"way","osm_id":5013364,"lat":"48.8582599","lon":"2.2945006","category":"man_made","type":"tower","place_rank":30,"importance":0.6205937724353116,"addresstype":"man_made","name":"Tour Eiffel","display_name":"Tour Eiffel, 5, Avenue Anatole France, Quartier du Gros-Caillou, Paris 7e Arrondissement, Paris, Île-de-France, France métropolitaine, 75007, France","address":{"man_made":"Tour Eiffel","house_number":"5","road":"Avenue Anatole France","city_block":"Quartier du Gros-Caillou","suburb":"Paris 7e Arrondissement","city_district":"Paris","city":"Paris","ISO3166-2-lvl6":"FR-75C","state":"Île-de-France","ISO3166-2-lvl4":"FR-IDF","region":"France métropolitaine","postcode":"75007","country":"France","country_code":"fr"},"extratags":{"fee": "10-25€", "3dmr": "4", "image": "https://fr.wikipedia.org/wiki/Fichier:Tour_Eiffel_Wikimedia_Commons.jpg", "layer": "2", "height": "330", "image:0": "https://photos.app.goo.gl/y43gmbzPz7jToV9D7", "ref:mhs": "PA00088801", "tourism": "attraction", "website": "https://www.toureiffel.paris/", "building": "tower", "engineer": "Gustave Eiffel;Maurice Koechlin;Émile Nouguier", "heritage": "3", "historic": "monument", "landmark": "1", "operator": "Société d’Exploitation de la Tour Eiffel", "wikidata": "Q243", "architect": "Stephen Sauvestre", "panoramax": "b57848f0-a526-4bab-8cb3-ede3df7b41c3;7e69687f-3956-41f2-bd85-03af73df2b2b;7ca4fd32-0e0a-498f-826c-d106ee360948&xyz=3.20/53.64/30", "wikipedia": "fr:Tour Eiffel", "importance": "international", "min_height": "0", "start_date": "1889", "tower:type": "communication;observation", "wheelchair": "no", "ref:FR:ANFR": "548894", "opening_hours": "09:30-23:45; Jun 21-Sep 2: 09:00-00:45; Jul 14,Jul 15 off", "building:shape": "pyramidal", "ref:FR:FANTOIR": "75107P320D", "building:colour": "#706550", "historic:period": "modern", "tourism:visitors": "7000000", "building:material": "iron", "heritage:operator": "mhs", "wikimedia_commons": "Category:Eiffel Tower", "tower:construction": "lattice", "communication:radio": "fm", "mhs:inscription_date": "1964-06-24", "communication:television": "dvb-t"},"boundingbox":["48.8574753","48.8590453","2.2933119","2.2956897"]},{"place_id":89203605,"licence":"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"node","osm_id":3135278479,"lat":"48.8581328","lon":"2.2944968","category":"amenity","type":"restaurant","place_rank":30,"importance":0.2917175836992075,"addresstype":"amenity","name":"Le Jules Verne","display_name":"Le Jules Verne, Avenue Gustave Eiffel, Quartier du Gros-Caillou, Paris 7e Arrondissement, Paris, Île-de-France, France métropolitaine, 75007, France","address":{"amenity":"Le Jules Verne","road":"Avenue Gustave Eiffel","city_block":"Quartier du Gros-Caillou","suburb":"Paris 7e Arrondissement","city_district":"Paris","city":"Paris","ISO3166-2-lvl6":"FR-75C","state":"Île-de-France","ISO3166-2-lvl4":"FR-IDF","region":"France métropolitaine","postcode":"75007","country":"France","country_code":"fr"},"extratags":{"level": "2", "stars": "1", "cuisine": "french", "website": "https://www.lejulesverne-paris.com", "wikidata": "Q3223818", "check_date": "2025-10-23", "wheelchair": "yes", "indoor_seating": "no", "outdoor_seating": "yes"},"boundingbox":["48.8580828","48.8581828","2.2944468","2.2945468"]}]', ['response_headers' => ['content-type' => 'application/json']]); } self::fail(sprintf('Unexpected http call "%s %s".', $method, $url)); diff --git a/tests/Mapping/Driver/AttributeDriverTest.php b/tests/Mapping/Driver/AttributeDriverTest.php index 2e3d7c1b..00890dd3 100644 --- a/tests/Mapping/Driver/AttributeDriverTest.php +++ b/tests/Mapping/Driver/AttributeDriverTest.php @@ -42,6 +42,26 @@ public function testLoadMetadata(): void self::assertSame('latitude', $metadata->latitudeProperty->getName()); self::assertNotNull($metadata->longitudeProperty); self::assertSame('longitude', $metadata->longitudeProperty->getName()); + self::assertNotNull($metadata->northProperty); + self::assertSame('north', $metadata->northProperty->getName()); + self::assertNotNull($metadata->southProperty); + self::assertSame('south', $metadata->southProperty->getName()); + self::assertNotNull($metadata->eastProperty); + self::assertSame('east', $metadata->eastProperty->getName()); + self::assertNotNull($metadata->westProperty); + self::assertSame('west', $metadata->westProperty->getName()); + self::assertNotNull($metadata->streetNumberProperty); + self::assertSame('streetNumber', $metadata->streetNumberProperty->getName()); + self::assertNotNull($metadata->streetNameProperty); + self::assertSame('streetName', $metadata->streetNameProperty->getName()); + self::assertNotNull($metadata->localityProperty); + self::assertSame('locality', $metadata->localityProperty->getName()); + self::assertNotNull($metadata->postalCodeProperty); + self::assertSame('postalCode', $metadata->postalCodeProperty->getName()); + self::assertNotNull($metadata->subLocalityProperty); + self::assertSame('subLocality', $metadata->subLocalityProperty->getName()); + self::assertNotNull($metadata->countryProperty); + self::assertSame('country', $metadata->countryProperty->getName()); } public function testLoadMetadataWithAddressGetter(): void diff --git a/tests/Mapping/Driver/Fixtures/Dummy.php b/tests/Mapping/Driver/Fixtures/Dummy.php index aa70eedb..c76c7987 100644 --- a/tests/Mapping/Driver/Fixtures/Dummy.php +++ b/tests/Mapping/Driver/Fixtures/Dummy.php @@ -13,9 +13,19 @@ namespace Bazinga\GeocoderBundle\Tests\Mapping\Driver\Fixtures; use Bazinga\GeocoderBundle\Mapping\Attributes\Address; +use Bazinga\GeocoderBundle\Mapping\Attributes\Country; +use Bazinga\GeocoderBundle\Mapping\Attributes\East; use Bazinga\GeocoderBundle\Mapping\Attributes\Geocodeable; use Bazinga\GeocoderBundle\Mapping\Attributes\Latitude; +use Bazinga\GeocoderBundle\Mapping\Attributes\Locality; use Bazinga\GeocoderBundle\Mapping\Attributes\Longitude; +use Bazinga\GeocoderBundle\Mapping\Attributes\North; +use Bazinga\GeocoderBundle\Mapping\Attributes\PostalCode; +use Bazinga\GeocoderBundle\Mapping\Attributes\South; +use Bazinga\GeocoderBundle\Mapping\Attributes\StreetName; +use Bazinga\GeocoderBundle\Mapping\Attributes\StreetNumber; +use Bazinga\GeocoderBundle\Mapping\Attributes\SubLocality; +use Bazinga\GeocoderBundle\Mapping\Attributes\West; #[Geocodeable(provider: 'acme')] final class Dummy @@ -28,4 +38,34 @@ final class Dummy #[Address] public ?string $address; + + #[North] + public ?float $north; + + #[South] + public ?float $south; + + #[East] + public ?float $east; + + #[West] + public ?float $west; + + #[StreetNumber] + public int|string|null $streetNumber; + + #[StreetName] + public ?string $streetName; + + #[Locality] + public ?string $locality; + + #[PostalCode] + public ?string $postalCode; + + #[SubLocality] + public ?string $subLocality; + + #[Country] + public ?string $country; } From f9d6ab37dddcdcd4be29b3fcbd48d33e33c2f05d Mon Sep 17 00:00:00 2001 From: Aymeric Mayeux Date: Mon, 9 Feb 2026 10:14:32 +0100 Subject: [PATCH 2/2] fix: disable ORM native lazy object on Symfony 6.4 in tests Disable native lazy objects in tests for Symfony 6.4 as they do not seems to work well with the testsuite. Also ignore a specific PHPStan error that has been reported as a bug. --- phpstan-baseline.php | 7 +++++++ tests/Functional/GeocodeEntityListenerTest.php | 11 ++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 4c759080..afb501b5 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -207,5 +207,12 @@ 'count' => 1, 'path' => __DIR__.'/src/ProviderFactory/GeoIP2Factory.php', ]; +// See https://github.com/phpstan/phpstan/issues/14067 +$ignoreErrors[] = [ + 'message' => '#^Parameter \\#1 \\$provider of class Bazinga\\\\GeocoderBundle\\\\Mapping\\\\ClassMetadata constructor expects non-empty-string, ReflectionMethod\\|ReflectionProperty\\|non-empty-string given\\.$#', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => __DIR__.'/src/Mapping/Driver/AttributeDriver.php', +]; return ['parameters' => ['ignoreErrors' => $ignoreErrors]]; diff --git a/tests/Functional/GeocodeEntityListenerTest.php b/tests/Functional/GeocodeEntityListenerTest.php index 166dfda6..e63eb154 100644 --- a/tests/Functional/GeocodeEntityListenerTest.php +++ b/tests/Functional/GeocodeEntityListenerTest.php @@ -92,12 +92,17 @@ public function process(ContainerBuilder $container): void $orm['enable_lazy_ghost_objects'] = true; } if (\PHP_VERSION_ID >= 80400 - && version_compare($doctrineBundleVersion, '2.15.0', '>=') - && version_compare($doctrineBundleVersion, '3.1.0', '<') - && version_compare($ormVersion, '3.4.0', '>=') + && version_compare($doctrineBundleVersion, '2.15.0', '>=') + && version_compare($doctrineBundleVersion, '3.1.0', '<') + && version_compare($ormVersion, '3.4.0', '>=') ) { $orm['enable_native_lazy_objects'] = true; } + + if (Kernel::VERSION_ID <= 70000) { + $orm['enable_lazy_ghost_objects'] = false; + $orm['enable_native_lazy_objects'] = false; + } } $container->prependExtensionConfig('doctrine', [ 'orm' => $orm,