Installation:
composer require craue/geo-bundle
Enable the bundle in config/bundles.php:
return [
// ...
Craue\GeoBundle\CraueGeoBundle::class => ['all' => true],
];
Database Setup:
Run the migration to create the geo_postal_code table (required for GEO_DISTANCE_BY_POSTAL_CODE):
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
Populate Postal Data (if using postal codes):
Use the provided fixtures or manually insert data into geo_postal_code (e.g., country, postal code, latitude, longitude).
First Query:
Add latitude/longitude fields to your entity (e.g., User):
// src/Entity/User.php
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class User {
#[ORM\Column(type: 'decimal', precision: 10, scale: 8)]
private $latitude;
#[ORM\Column(type: 'decimal', precision: 11, scale: 8)]
private $longitude;
}
Query distance in DQL:
$distance = $entityManager->createQuery(
'SELECT GEO_DISTANCE(u.latitude, u.longitude, :lat, :lng) AS distance FROM App\Entity\User u'
)->setParameter('lat', 48.8566)
->setParameter('lng', 2.3522)
->getSingleScalarResult();
Distance Queries:
GEO_DISTANCE(lat1, lon1, lat2, lon2) for direct coordinates.
$query = $em->createQuery(
'SELECT u, GEO_DISTANCE(u.latitude, u.longitude, :lat, :lng) AS distance
FROM App\Entity\User u
WHERE GEO_DISTANCE(u.latitude, u.longitude, :lat, :lng) < :maxDistance
ORDER BY distance ASC'
);
GEO_DISTANCE_BY_POSTAL_CODE(:country1, :postal1, :country2, :postal2).
$query = $em->createQuery(
'SELECT GEO_DISTANCE_BY_POSTAL_CODE(:country, :postal, "FR", "75000") AS distance
FROM App\Entity\Store'
);
Entity Integration:
// src/Entity/User.php
public function getDistanceTo(User $other): float {
return $this->getEntityManager()
->createQueryBuilder()
->select('GEO_DISTANCE(u.latitude, u.longitude, :lat, :lng) AS distance')
->setParameter('lat', $other->getLatitude())
->setParameter('lng', $other->getLongitude())
->getQuery()
->getSingleScalarResult();
}
Performance:
CREATE INDEX idx_user_geo ON user (latitude, longitude);
GEO_DISTANCE_BY_POSTAL_CODE.Symfony Forms:
// src/Form/UserType.php
$builder->add('latitude', HiddenType::class, [
'data' => $user->getLatitude(),
]);
$builder->add('longitude', HiddenType::class, [
'data' => $user->getLongitude(),
]);
API Endpoints:
// src/Controller/UserController.php
#[Route('/users/nearby', name: 'users_nearby', methods: ['GET'])]
public function nearbyUsers(Request $request, EntityManagerInterface $em): JsonResponse {
$lat = $request->query->get('lat');
$lng = $request->query->get('lng');
$distance = $em->createQuery(
'SELECT u, GEO_DISTANCE(u.latitude, u.longitude, :lat, :lng) AS distance
FROM App\Entity\User u
WHERE GEO_DISTANCE(u.latitude, u.longitude, :lat, :lng) < 10
ORDER BY distance ASC'
)->setParameter('lat', $lat)
->setParameter('lng', $lng)
->getResult();
return $this->json($distance);
}
Postal Code Data:
GEO_DISTANCE_BY_POSTAL_CODE requires pre-populated geo_postal_code table. Without it, queries fail silently or return NULL.
Precision Issues:
decimal(10,8) may lose precision for very small distances (e.g., <1m). Adjust precision if needed:
#[ORM\Column(type: 'decimal', precision: 15, scale: 10)]
private $latitude;
Database Compatibility:
GEOMETRY functions under the hood. Ensure your MySQL version supports it (5.7+ recommended).earthdistance() for better accuracy (not supported by this bundle).Performance:
JOIN or SELECT only the needed fields:
// Bad: Loads all user data
$query = $em->createQuery('SELECT u FROM App\Entity\User u WHERE GEO_DISTANCE(...) < 10');
// Good: Loads only IDs/distance
$query = $em->createQuery('SELECT u.id, GEO_DISTANCE(...) AS distance FROM App\Entity\User u WHERE distance < 10');
Time Zones:
Query Logging: Enable Doctrine logging to debug DQL:
// config/packages/dev/doctrine.yaml
doctrine:
dbal:
logging: true
profiling: true
Check logs for malformed queries (e.g., missing parameters).
Postal Code Lookup: Verify postal code data exists:
$postal = $em->getRepository(Craue\GeoBundle\Entity\GeoPostalCode::class)
->findOneBy(['country' => 'FR', 'postalCode' => '75000']);
Distance Units:
The bundle returns distances in kilometers. Multiply by 0.621371 for miles if needed.
Bulk Updates: Use a script to update latitude/longitude for existing records (e.g., via Google Maps API):
// src/Command/UpdateGeoData.php
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class UpdateGeoData extends Command {
protected function execute(InputInterface $input, OutputInterface $output): int {
$io = new SymfonyStyle($input, $output);
$users = $this->getDoctrine()->getRepository(User::class)->findAll();
foreach ($users as $user) {
$address = $user->getAddress();
$geoData = $this->geocodeAddress($address); // Use a geocoding service
$user->setLatitude($geoData['lat']);
$user->setLongitude($geoData['lng']);
$this->getDoctrine()->getManager()->flush();
$io->text(sprintf('Updated %s', $user->getEmail()));
}
return Command::SUCCESS;
}
}
Custom Functions:
Extend the bundle by adding your own Doctrine functions. Override the Craue\GeoBundle\DependencyInjection\Compiler\GeoFunctionsPass compiler pass:
# config/packages/craue_geo.yaml
craue_geo:
custom_functions:
GEO_BEARING: 'Craue\GeoBundle\Doctrine\GeoFunctions::bearing'
How can I help you explore Laravel packages today?