本文目录导读:

我来详细介绍Symfony表单与地理位置的结合使用。
基础设置
安装依赖包
composer require lexik/form-filter-bundle composer require geoip2/geoip2 composer require stof/doctrine-extensions-bundle
配置地理信息服务
# config/packages/lexik_form_filter.yaml
lexik_form_filter:
listeners:
force_parameter_type: false
地理位置表单字段类型
创建地理位置表单类型
// src/Form/Type/GeolocationType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\CallbackTransformer;
class GeolocationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('latitude', HiddenType::class, [
'required' => false,
'attr' => ['class' => 'latitude-input']
])
->add('longitude', HiddenType::class, [
'required' => false,
'attr' => ['class' => 'longitude-input']
])
->add('address', null, [
'required' => false,
'label' => '地址',
'attr' => [
'placeholder' => '输入地址搜索',
'class' => 'geolocation-address'
]
]);
// 添加数据转换
$builder->addModelTransformer(new CallbackTransformer(
function ($location) {
// 实体到表单
if ($location instanceof Location) {
return [
'latitude' => $location->getLatitude(),
'longitude' => $location->getLongitude(),
'address' => $location->getAddress()
];
}
return ['latitude' => null, 'longitude' => null, 'address' => null];
},
function ($data) {
// 表单到实体
if (is_array($data) && isset($data['latitude'])) {
$location = new Location();
$location->setLatitude($data['latitude']);
$location->setLongitude($data['longitude']);
$location->setAddress($data['address'] ?? '');
return $location;
}
return null;
}
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => null,
'compound' => true,
]);
}
}
实体类设计
地理位置实体
// src/Entity/Location.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Embeddable]
class Location
{
#[ORM\Column(type: 'decimal', precision: 10, scale: 7, nullable: true)]
#[Assert\Range(min: -90, max: 90)]
private ?float $latitude = null;
#[ORM\Column(type: 'decimal', precision: 10, scale: 7, nullable: true)]
#[Assert\Range(min: -180, max: 180)]
private ?float $longitude = null;
#[ORM\Column(type: 'string', length: 255, nullable: true)]
private ?string $address = null;
// Getters and Setters
public function getLatitude(): ?float
{
return $this->latitude;
}
public function setLatitude(?float $latitude): self
{
$this->latitude = $latitude;
return $this;
}
public function getLongitude(): ?float
{
return $this->longitude;
}
public function setLongitude(?float $longitude): self
{
$this->longitude = $longitude;
return $this;
}
public function getAddress(): ?string
{
return $this->address;
}
public function setAddress(?string $address): self
{
$this->address = $address;
return $this;
}
public function getCoordinates(): ?array
{
if ($this->latitude && $this->longitude) {
return ['lat' => $this->latitude, 'lng' => $this->longitude];
}
return null;
}
}
主实体示例
// src/Entity/Place.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Place
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Embedded(class: Location::class)]
private ?Location $location = null;
public function __construct()
{
$this->location = new Location();
}
// Getters and Setters
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getLocation(): ?Location
{
return $this->location;
}
public function setLocation(Location $location): self
{
$this->location = $location;
return $this;
}
}
控制器实现
PlaceController
// src/Controller/PlaceController.php
namespace App\Controller;
use App\Entity\Place;
use App\Form\Type\PlaceType;
use App\Repository\PlaceRepository;
use App\Service\GeolocationService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/places')]
class PlaceController extends AbstractController
{
#[Route('/new', name: 'place_new', methods: ['GET', 'POST'])]
public function new(Request $request, GeolocationService $geolocationService, EntityManagerInterface $em): Response
{
$place = new Place();
$form = $this->createForm(PlaceType::class, $place);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// 验证地理位置
$location = $place->getLocation();
if ($location->getLatitude() && $location->getLongitude()) {
// 反向地理编码获取地址
$address = $geolocationService->reverseGeocode(
$location->getLatitude(),
$location->getLongitude()
);
$location->setAddress($address);
}
$em->persist($place);
$em->flush();
$this->addFlash('success', '地点创建成功');
return $this->redirectToRoute('place_index');
}
return $this->render('place/new.html.twig', [
'form' => $form->createView(),
]);
}
#[Route('/search', name: 'place_search', methods: ['GET'])]
public function search(Request $request, PlaceRepository $placeRepository): Response
{
$form = $this->createForm(PlaceSearchType::class);
$form->handleRequest($request);
$places = [];
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
$places = $placeRepository->findNearby(
$data['latitude'],
$data['longitude'],
$data['radius'] ?? 10
);
}
return $this->render('place/search.html.twig', [
'form' => $form->createView(),
'places' => $places,
]);
}
}
搜索表单与距离查询
搜索表单类型
// src/Form/Type/PlaceSearchType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\RangeType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PlaceSearchType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('latitude', NumberType::class, [
'required' => true,
'label' => '纬度'
])
->add('longitude', NumberType::class, [
'required' => true,
'label' => '经度'
])
->add('radius', RangeType::class, [
'required' => true,
'label' => '搜索半径 (公里)',
'attr' => [
'min' => 1,
'max' => 50,
'step' => 1
]
]);
}
}
带距离查询的Repository
// src/Repository/PlaceRepository.php
namespace App\Repository;
use App\Entity\Place;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class PlaceRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Place::class);
}
public function findNearby(float $latitude, float $longitude, float $radius = 10): array
{
$earthRadius = 6371; // 地球半径(公里)
$sql = "
SELECT p,
( :earthRadius * ACOS(
COS(RADIANS(:latitude)) *
COS(RADIANS(p.location.latitude)) *
COS(RADIANS(p.location.longitude) - RADIANS(:longitude)) +
SIN(RADIANS(:latitude)) *
SIN(RADIANS(p.location.latitude))
) ) AS distance
FROM App\Entity\Place p
WHERE p.location.latitude IS NOT NULL
AND p.location.longitude IS NOT NULL
HAVING distance <= :radius
ORDER BY distance ASC
";
$query = $this->getEntityManager()
->createQuery($sql)
->setParameter('earthRadius', $earthRadius)
->setParameter('latitude', $latitude)
->setParameter('longitude', $longitude)
->setParameter('radius', $radius);
return $query->getResult();
}
}
JavaScript前端集成
前端JavaScript
// assets/js/geolocation.js
class GeolocationHelper {
constructor(formId) {
this.form = document.getElementById(formId);
this.addressInput = this.form.querySelector('.geolocation-address');
this.latitudeInput = this.form.querySelector('.latitude-input');
this.longitudeInput = this.form.querySelector('.longitude-input');
this.initAutocomplete();
this.initGeoLocation();
}
initAutocomplete() {
// 使用Google Maps Autocomplete
if (typeof google !== 'undefined') {
const autocomplete = new google.maps.places.Autocomplete(this.addressInput);
autocomplete.addListener('place_changed', () => {
const place = autocomplete.getPlace();
if (place.geometry) {
this.latitudeInput.value = place.geometry.location.lat();
this.longitudeInput.value = place.geometry.location.lng();
this.addressInput.value = place.formatted_address;
}
});
}
}
initGeoLocation() {
if (navigator.geolocation) {
const getLocationBtn = document.getElementById('get-location-btn');
if (getLocationBtn) {
getLocationBtn.addEventListener('click', () => {
navigator.geolocation.getCurrentPosition(
(position) => {
this.latitudeInput.value = position.coords.latitude;
this.longitudeInput.value = position.coords.longitude;
this.reverseGeocode(
position.coords.latitude,
position.coords.longitude
);
},
(error) => {
console.error('GeoLocation Error:', error);
}
);
});
}
}
}
async reverseGeocode(lat, lng) {
try {
const response = await fetch(`/api/reverse-geocode?lat=${lat}&lng=${lng}`);
const data = await response.json();
if (data.address) {
this.addressInput.value = data.address;
}
} catch (error) {
console.error('Reverse Geocode Error:', error);
}
}
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
new GeolocationHelper('place_form');
});
模板文件
Twig模板
{# templates/place/new.html.twig #}
{% extends 'base.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
{% endblock %}
{% block body %}
<div class="container mt-4">
<h1>创建新地点</h1>
{{ form_start(form) }}
<div class="card">
<div class="card-body">
<div class="mb-3">
{{ form_label(form.name) }}
{{ form_widget(form.name) }}
</div>
<div class="mb-3">
{{ form_label(form.location) }}
<div id="map" style="height: 400px;" class="mb-3"></div>
{{ form_widget(form.location) }}
<button type="button" id="get-location-btn" class="btn btn-secondary mt-2">
获取当前位置
</button>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary mt-3">保存</button>
{{ form_end(form) }}
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places"></script>
<script src="{{ asset('js/geolocation.js') }}"></script>
<script>
// 初始化Leaflet地图
var map = L.map('map').setView([39.9042, 116.4074], 10);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
var marker;
map.on('click', function(e) {
if (marker) {
map.removeLayer(marker);
}
marker = L.marker(e.latlng).addTo(map);
document.querySelector('.latitude-input').value = e.latlng.lat;
document.querySelector('.longitude-input').value = e.latlng.lng;
});
</script>
{% endblock %}
API服务
反向地理编码服务
// src/Service/GeolocationService.php
namespace App\Service;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class GeolocationService
{
private $client;
public function __construct(HttpClientInterface $client)
{
$this->client = $client;
}
public function reverseGeocode(float $latitude, float $longitude): ?string
{
try {
$response = $this->client->request('GET', 'https://nominatim.openstreetmap.org/reverse', [
'query' => [
'lat' => $latitude,
'lon' => $longitude,
'format' => 'json',
'addressdetails' => 1,
]
]);
$data = $response->toArray();
return $data['display_name'] ?? null;
} catch (\Exception $e) {
return null;
}
}
public function geocode(string $address): ?array
{
try {
$response = $this->client->request('GET', 'https://nominatim.openstreetmap.org/search', [
'query' => [
'q' => $address,
'format' => 'json',
'limit' => 1,
]
]);
$data = $response->toArray();
if (!empty($data)) {
return [
'latitude' => $data[0]['lat'],
'longitude' => $data[0]['lon'],
'address' => $data[0]['display_name']
];
}
return null;
} catch (\Exception $e) {
return null;
}
}
}
自定义表单主题
# config/packages/twig.yaml
twig:
form_themes:
- 'form/geolocation_fields.html.twig'
{# templates/form/geolocation_fields.html.twig #}
{% block geolocation_widget %}
<div class="geolocation-widget">
<div class="mb-3">
{{ form_widget(form.address, {'attr': {'class': 'form-control geolocation-address', 'placeholder': '输入地址...'}}) }}
</div>
<div class="row">
<div class="col-md-6">
{{ form_label(form.latitude, '纬度') }}
{{ form_widget(form.latitude, {'attr': {'class': 'form-control latitude-input'}}) }}
</div>
<div class="col-md-6">
{{ form_label(form.longitude, '经度') }}
{{ form_widget(form.longitude, {'attr': {'class': 'form-control longitude-input'}}) }}
</div>
</div>
<div id="map-container" style="height: 300px;" class="mt-3"></div>
</div>
{% endblock %}
验证约束
// src/Validator/Constraints/ValidLocation.php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
#[Attribute]
class ValidLocation extends Constraint
{
public $message = '请提供有效的地理位置坐标。';
public $latitudeRangeMessage = '纬度值必须在 -90 到 90 之间。';
public $longitudeRangeMessage = '经度值必须在 -180 到 180 之间。';
}
// src/Validator/Constraints/ValidLocationValidator.php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class ValidLocationValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof ValidLocation) {
return;
}
if ($value === null || $value === '') {
return;
}
$latitude = $value->getLatitude();
$longitude = $value->getLongitude();
if ($latitude !== null && ($latitude < -90 || $latitude > 90)) {
$this->context->buildViolation($constraint->latitudeRangeMessage)
->atPath('latitude')
->addViolation();
}
if ($longitude !== null && ($longitude < -180 || $longitude > 180)) {
$this->context->buildViolation($constraint->longitudeRangeMessage)
->atPath('longitude')
->addViolation();
}
}
}
最佳实践建议
- 使用覆盖嵌入(Embedded):将地理位置作为值对象处理
- 前端验证:确保前端也进行坐标验证
- 数据库索引:为经纬度字段创建索引
- API限流:使用外部地理编码服务时注意API调用限制
- 缓存:缓存地理编码结果
- 渐进增强:确保无JavaScript时也能使用基本功能
这样你就可以在Symfony项目中实现完整的地理位置表单功能了!
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播