PHP项目Symfony form与级联下拉

wen PHP项目 1

本文目录导读:

PHP项目Symfony form与级联下拉

  1. 基础实体关系
  2. 创建Form类型
  3. 控制器实现
  4. Twig模板
  5. 完整示例:三级级联(省/市/区)
  6. 使用JavaScript动态更新(高级版)
  7. Symfony 5/6 建议使用的优化方式

我来为你详细介绍 Symfony Form 中级联下拉菜单的实现方法。

基础实体关系

首先创建具有级联关系的实体:

// src/Entity/Country.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CountryRepository::class)]
class Country
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;
    #[ORM\Column(length: 100)]
    private ?string $name = null;
    // getters/setters
}
// src/Entity/City.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CityRepository::class)]
class City
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;
    #[ORM\Column(length: 100)]
    private ?string $name = null;
    #[ORM\ManyToOne(targetEntity: Country::class)]
    #[ORM\JoinColumn(nullable: false)]
    private ?Country $country = null;
    // getters/setters
}

创建Form类型

// src/Form/AddressType.php
namespace App\Form;
use App\Entity\Address;
use App\Entity\Country;
use App\Entity\City;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AddressType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('country', EntityType::class, [
            'class' => Country::class,
            'placeholder' => '请选择国家',
            'mapped' => false, // 不直接映射到实体
            'required' => true,
            'label' => '国家'
        ]);
        // 添加表单监听器
        $formModifier = function (FormInterface $form, ?Country $country = null) {
            $cities = null === $country ? [] : $country->getCities();
            $form->add('city', EntityType::class, [
                'class' => City::class,
                'placeholder' => '请先选择国家',
                'choices' => $cities,
                'required' => true,
                'label' => '城市'
            ]);
        };
        // 监听表单创建事件
        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($formModifier) {
                $data = $event->getData();
                $country = $data?->getCountry();
                $formModifier($event->getForm(), $country);
            }
        );
        // 监听表单提交事件
        $builder->get('country')->addEventListener(
            FormEvents::POST_SUBMIT,
            function (FormEvent $event) use ($formModifier) {
                $country = $event->getForm()->getData();
                $formModifier($event->getForm()->getParent(), $country);
            }
        );
    }
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Address::class,
        ]);
    }
}

控制器实现

// src/Controller/AddressController.php
namespace App\Controller;
use App\Entity\Address;
use App\Form\AddressType;
use App\Repository\CityRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class AddressController extends AbstractController
{
    #[Route('/address/new', name: 'address_new')]
    public function new(Request $request): Response
    {
        $address = new Address();
        $form = $this->createForm(AddressType::class, $address);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            // 处理表单数据
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($address);
            $entityManager->flush();
            return $this->redirectToRoute('address_list');
        }
        return $this->render('address/new.html.twig', [
            'form' => $form->createView(),
        ]);
    }
    // AJAX端点 - 根据国家获取城市
    #[Route('/get-cities-by-country', name: 'get_cities_by_country', methods: ['POST'])]
    public function getCitiesByCountry(Request $request, CityRepository $cityRepository): JsonResponse
    {
        $countryId = $request->request->get('country_id');
        if (!$countryId) {
            return new JsonResponse([]);
        }
        $cities = $cityRepository->findBy(
            ['country' => $countryId],
            ['name' => 'ASC']
        );
        $data = [];
        foreach ($cities as $city) {
            $data[] = [
                'id' => $city->getId(),
                'name' => $city->getName()
            ];
        }
        return new JsonResponse($data);
    }
}

Twig模板

{# templates/address/new.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
    <div class="container mt-4">
        <h1>添加地址</h1>
        {{ form_start(form, {'attr': {'id': 'address_form'}}) }}
            {{ form_row(form.country, {
                'attr': {'class': 'form-control', 'id': 'country_select'}
            }) }}
            {{ form_row(form.city, {
                'attr': {'class': 'form-control', 'id': 'city_select'}
            }) }}
            <button type="submit" class="btn btn-primary">提交</button>
        {{ form_end(form) }}
    </div>
{% endblock %}
{% block javascripts %}
    {{ parent() }}
    <script>
        const $country = $('#country_select');
        const $city = $('#city_select');
        $country.on('change', function() {
            const countryId = $(this).val();
            if (countryId) {
                $.ajax({
                    url: '{{ path('get_cities_by_country') }}',
                    type: 'POST',
                    dataType: 'json',
                    data: {country_id: countryId},
                    success: function(data) {
                        $city.empty();
                        $city.append('<option value="">请选择城市</option>');
                        $.each(data, function(index, city) {
                            $city.append(
                                $('<option>', {
                                    value: city.id,
                                    text: city.name
                                })
                            );
                        });
                    }
                });
            } else {
                $city.empty();
                $city.append('<option value="">请先选择国家</option>');
            }
        });
    </script>
{% endblock %}

完整示例:三级级联(省/市/区)

// src/Form/AddressType.php (三级级联版本)
namespace App\Form;
use App\Entity\Address;
use App\Entity\Province;
use App\Entity\City;
use App\Entity\District;
use App\Repository\CityRepository;
use App\Repository\DistrictRepository;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AddressType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('province', EntityType::class, [
                'class' => Province::class,
                'placeholder' => '请选择省份',
                'required' => true,
                'label' => '省份'
            ]);
        $addCityField = function (FormInterface $form, ?Province $province = null) {
            $form->add('city', EntityType::class, [
                'class' => City::class,
                'placeholder' => '请选择城市',
                'query_builder' => function (EntityRepository $er) use ($province) {
                    $qb = $er->createQueryBuilder('c');
                    if ($province) {
                        $qb->where('c.province = :province')
                           ->setParameter('province', $province);
                    }
                    return $qb;
                },
                'required' => true,
                'label' => '城市'
            ]);
        };
        $addDistrictField = function (FormInterface $form, ?City $city = null) {
            $form->add('district', EntityType::class, [
                'class' => District::class,
                'placeholder' => '请选择区县',
                'query_builder' => function (EntityRepository $er) use ($city) {
                    $qb = $er->createQueryBuilder('d');
                    if ($city) {
                        $qb->where('d.city = :city')
                           ->setParameter('city', $city);
                    }
                    return $qb;
                },
                'required' => true,
                'label' => '区县'
            ]);
        };
        // 表单初始化
        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($addCityField, $addDistrictField) {
            $data = $event->getData();
            $form = $event->getForm();
            $province = $data?->getProvince();
            $city = $data?->getCity();
            $addCityField($form, $province);
            $addDistrictField($form, $city);
        });
        // 省份变化
        $builder->get('province')->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($addCityField, $addDistrictField) {
            $form = $event->getForm()->getParent();
            $province = $event->getForm()->getData();
            $addCityField($form, $province);
            $addDistrictField($form, null);
        });
        // 城市变化
        $builder->get('city')->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($addDistrictField) {
            $form = $event->getForm()->getParent();
            $city = $event->getForm()->getData();
            $addDistrictField($form, $city);
        });
    }
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Address::class,
        ]);
    }
}

使用JavaScript动态更新(高级版)

// public/js/cascading-dropdown.js
class CascadingDropdown {
    constructor(config) {
        this.parentField = $(config.parentSelector);
        this.childField = $(config.childSelector);
        this.url = config.url;
        this.parentFieldId = config.parentFieldId;
        this.childFieldId = config.childFieldId;
        this.placeholder = config.placeholder || '请选择';
        this.init();
    }
    init() {
        this.parentField.on('change', () => this.loadChildren());
        // 如果有初始值,自动加载
        if (this.parentField.val()) {
            this.loadChildren();
        }
    }
    loadChildren() {
        const parentId = this.parentField.val();
        if (!parentId) {
            this.clearChildren();
            return;
        }
        $.ajax({
            url: this.url,
            type: 'POST',
            data: {
                [this.parentFieldId]: parentId
            },
            dataType: 'json',
            beforeSend: () => {
                this.childField.prop('disabled', true);
                this.childField.html('<option value="">加载中...</option>');
            },
            success: (data) => {
                this.populateChildren(data);
            },
            error: () => {
                this.childField.html('<option value="">加载失败</option>');
            },
            complete: () => {
                this.childField.prop('disabled', false);
            }
        });
    }
    populateChildren(data) {
        this.childField.empty();
        this.childField.append(`<option value="">${this.placeholder}</option>`);
        $.each(data, (index, item) => {
            this.childField.append(
                $('<option>', {
                    value: item.id,
                    text: item.name
                })
            );
        });
    }
    clearChildren() {
        this.childField.empty();
        this.childField.append(`<option value="">${this.placeholder}</option>`);
    }
}
// 使用示例
$(document).ready(function() {
    new CascadingDropdown({
        parentSelector: '#province_select',
        childSelector: '#city_select',
        url: '/get-cities',
        parentFieldId: 'province_id',
        childFieldId: 'city',
        placeholder: '请选择城市'
    });
    new CascadingDropdown({
        parentSelector: '#city_select',
        childSelector: '#district_select',
        url: '/get-districts',
        parentFieldId: 'city_id',
        childFieldId: 'district',
        placeholder: '请选择区县'
    });
});

Symfony 5/6 建议使用的优化方式

// 使用 LiveComponent (Symfony UX)
// composer require symfony/ux-live-component
// src/Components/AddressFormComponent.php
namespace App\Components;
use App\Entity\Address;
use App\Entity\City;
use App\Entity\Country;
use App\Repository\CityRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
#[AsLiveComponent('address_form')]
class AddressFormComponent extends AbstractController
{
    use DefaultActionTrait;
    #[LiveProp]
    public ?Country $selectedCountry = null;
    #[LiveProp]
    public ?City $selectedCity = null;
    public function getCities(CityRepository $cityRepository): array
    {
        if ($this->selectedCountry) {
            return $cityRepository->findBy(['country' => $this->selectedCountry]);
        }
        return [];
    }
    #[LiveAction]
    public function updateCities(): void
    {
        $this->selectedCity = null;
    }
}
  1. 实体关系:确保关联实体正确配置
  2. 表单事件:使用 PRE_SET_DATAPOST_SUBMIT 事件
  3. AJAX更新:通过 JavaScript 动态加载下拉选项
  4. 数据验证:确保后端验证前端提交的数据
  5. 用户体验:添加加载状态和占位符

这样实现后,就可以在 Symfony 中轻松创建级联下拉菜单了。

抱歉,评论功能暂时关闭!