src/Form/UserProfileType.php line 339

  1. <?php
  2. namespace App\Form;
  3. use App\Entity\Centers;
  4. use App\Entity\User;
  5. use App\Entity\UserProfile;
  6. use App\Entity\TaxonomyValue;
  7. use App\Services\AddressCatalogService;
  8. use App\Services\RoleAccessService;
  9. use Symfony\Component\Form\AbstractType;
  10. use Symfony\Component\Form\FormBuilderInterface;
  11. use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
  12. use Symfony\Component\Form\Extension\Core\Type\DateType;
  13. use Symfony\Component\Form\Extension\Core\Type\TextType;
  14. use Symfony\Bridge\Doctrine\Form\Type\EntityType;
  15. use Symfony\Component\OptionsResolver\OptionsResolver;
  16. use Doctrine\ORM\EntityRepository;
  17. use Symfony\Component\Form\Extension\Core\Type\EmailType;
  18. use Symfony\Component\Form\FormEvent;
  19. use Symfony\Component\Form\FormEvents;
  20. use Symfony\Component\Form\FormInterface;
  21. use Symfony\Component\Validator\Constraints\Count;
  22. use Symfony\Component\Validator\Constraints\NotBlank;
  23. use Symfony\Component\Validator\Constraints\NotNull;
  24. use Symfony\Component\Validator\Constraints\Regex;
  25. use Symfony\Component\Security\Core\Security;
  26. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  27. class UserProfileType extends AbstractType
  28. {
  29.     public function __construct(
  30.         private Security $security,
  31.         private RoleAccessService $roleAccessService,
  32.         private AddressCatalogService $addressCatalogService,
  33.         private UrlGeneratorInterface $urlGenerator
  34.     ) {
  35.     }
  36.     public function buildForm(FormBuilderInterface $builder, array $options): void
  37.     {
  38.         $mode $options['mode']; // alta | edit | view
  39.         $profileContext $options['profile_context'];
  40.         $isPublicSignup $profileContext === 'public_signup';
  41.         $showTrainerType $profileContext === 'admin_courseuser';
  42.         $showStudentRoles $profileContext === 'admin_student';
  43.         $actor $this->security->getUser();
  44.         $canManageTrainerCenter $showTrainerType && (
  45.             $this->security->isGranted('ROLE_SUPERUSER') || $this->security->isGranted('ROLE_CENTER')
  46.         );
  47.         $originalProfile $builder->getData() instanceof UserProfile $builder->getData() : null;
  48.         $allDisabled $mode === 'view';
  49.         $builder
  50.             // DNI
  51.             ->add('dni'TextType::class, [
  52.                 'mapped' => false,
  53.                 'disabled' => $mode !== 'alta'// SOLO editable en alta
  54.                 'constraints' => [
  55.                     new NotBlank(['message' => 'El DNI/NIE es obligatorio.']),
  56.                     new Regex([
  57.                         'pattern' => '/^(?:\\d{8}|[XYZ]\\d{7})[A-Z]$/i',
  58.                         'message' => 'El DNI/NIE debe tener el formato correcto.',
  59.                     ]),
  60.                 ],
  61.             ])
  62.             // EMAIL
  63.             ->add('email'EmailType::class, [
  64.                 'mapped' => false,
  65.                 'disabled' => $mode !== 'alta'// SOLO editable en alta
  66.             ]);
  67.         if ($canManageTrainerCenter) {
  68.             $builder->add('managementCenter'EntityType::class, [
  69.                 'mapped' => false,
  70.                 'class' => Centers::class,
  71.                 'choice_label' => 'Name',
  72.                 'label' => 'Centro de gestión',
  73.                 'required' => false,
  74.                 'choices' => $actor instanceof User $this->roleAccessService->getAssignableCenters($actor) : [],
  75.                 'placeholder' => 'Seleccione centro',
  76.                 'disabled' => $allDisabled,
  77.                 'attr' => [
  78.                     'class' => 'no-selectpicker',
  79.                 ],
  80.             ]);
  81.         }
  82.         $builder
  83.             /* ================= DATOS PERSONALES ================= */
  84.             ->add('nombre'TextType::class, [
  85.                 'label' => 'Nombre',
  86.                 'disabled' => $allDisabled,
  87.             ])
  88.             ->add('apellido1'TextType::class, [
  89.                 'label' => 'Primer apellido',
  90.                 'disabled' => $allDisabled,
  91.             ])
  92.             ->add('apellido2'TextType::class, [
  93.                 'label' => 'Segundo apellido',
  94.                  'disabled' => $allDisabled,
  95.                 ])
  96.             ->add('fechaNacimiento'DateType::class, [
  97.                 'label' => 'Fecha de nacimiento',
  98.                 'widget' => 'single_text',
  99.                  'disabled' => $allDisabled,
  100.                 ])
  101.             ->add('sexo'ChoiceType::class, [
  102.                 'label' => 'Sexo (*)',
  103.                 'required' => true,
  104.                 'placeholder' => 'Seleccione sexo',
  105.                 'choices' => [
  106.                     'Hombre' => 'H',
  107.                     'Mujer'  => 'M',
  108.                 ],
  109.                 'disabled' => $allDisabled,
  110.                 'attr' => [
  111.                     'class' => 'no-selectpicker',
  112.                 ],
  113.             ]);
  114.         $this->addTipoViaField(
  115.             $builder,
  116.             $isPublicSignup,
  117.             $allDisabled,
  118.             $originalProfile?->getTipoVia(),
  119.             $originalProfile?->getTipoVia()
  120.         );
  121.         $this->addProvinceField(
  122.             $builder,
  123.             $isPublicSignup,
  124.             $allDisabled,
  125.             $originalProfile?->getProvincia(),
  126.             $originalProfile?->getProvincia()
  127.         );
  128.         $this->addMunicipalityField(
  129.             $builder,
  130.             $isPublicSignup,
  131.             $allDisabled,
  132.             $originalProfile?->getProvincia(),
  133.             $originalProfile?->getMunicipio(),
  134.             $originalProfile?->getProvincia(),
  135.             $originalProfile?->getMunicipio()
  136.         );
  137.         $builder
  138.             /* ================= DIRECCIÓN ================= */
  139.             ->add('nombreVia'TextType::class, [
  140.                 'label' => 'Nombre de la vía',
  141.                 'required' => $isPublicSignup,
  142.                 'disabled' => $allDisabled,
  143.                 'constraints' => [
  144.                     new NotBlank([
  145.                         'groups' => ['public_signup'],
  146.                         'message' => 'El nombre de la vía es obligatorio.',
  147.                     ]),
  148.                 ],
  149.             ])
  150.             ->add('numeroVia'TextType::class, [
  151.                 'label' => 'Número',
  152.                 'required' => $isPublicSignup,
  153.                 'disabled' => $allDisabled,
  154.                 'constraints' => [
  155.                     new NotBlank([
  156.                         'groups' => ['public_signup'],
  157.                         'message' => 'El número es obligatorio.',
  158.                     ]),
  159.                 ],
  160.             ])
  161.             ->add('codigoPostal'TextType::class, [
  162.                 'label' => 'Código postal',
  163.                 'required' => $isPublicSignup,
  164.                 'disabled' => $allDisabled,
  165.                 'constraints' => [
  166.                     new NotBlank([
  167.                         'groups' => ['public_signup'],
  168.                         'message' => 'El código postal es obligatorio.',
  169.                     ]),
  170.                 ],
  171.             ])
  172.             ->add('portal'TextType::class, [
  173.                 'label' => 'Portal',
  174.                 'required' => false,
  175.                 'disabled' => $allDisabled,
  176.                 ])
  177.             ->add('bloque'TextType::class, [
  178.                 'label' => 'Bloque',
  179.                 'required' => false,
  180.                 'disabled' => $allDisabled,
  181.                 ])
  182.             ->add('escalera'TextType::class, [
  183.                 'label' => 'Escalera',
  184.                 'required' => false,
  185.                 'disabled' => $allDisabled,
  186.                 ])
  187.             ->add('planta'TextType::class, [
  188.                 'label' => 'Planta',
  189.                 'required' => false,
  190.                 'disabled' => $allDisabled,
  191.                 ])
  192.             ->add('puerta'TextType::class, [
  193.                 'label' => 'Puerta',
  194.                 'required' => false,
  195.                 'disabled' => $allDisabled,
  196.                 ])
  197.             ->add('telefono'TextType::class, [
  198.                 'label' => 'Teléfono móvil',
  199.                 'required' => $isPublicSignup,
  200.                 'disabled' => $allDisabled,
  201.                 'constraints' => [
  202.                     new NotBlank([
  203.                         'groups' => ['public_signup'],
  204.                         'message' => 'El teléfono móvil es obligatorio.',
  205.                     ]),
  206.                 ],
  207.             ])
  208.             ->add('centroTrabajo'EntityType::class, [
  209.                 'label' => 'Centro de trabajo',
  210.                 'class' => TaxonomyValue::class,
  211.                 'choice_label' => 'label',
  212.                 'required' => $isPublicSignup,
  213.                 'placeholder' => 'Seleccione centro',
  214.                 'query_builder' => function (EntityRepository $er) {
  215.                     return $er->createQueryBuilder('tv')
  216.                         ->join('tv.taxonomy''t')
  217.                         ->where('t.name = :name')
  218.                         ->setParameter('name''user_profile_centro_trabajo')
  219.                         ->orderBy('tv.label''ASC');
  220.                 },
  221.                 'disabled' => $allDisabled,
  222.                 'attr' => [
  223.                     'class' => 'no-selectpicker',
  224.                 ],
  225.                 'constraints' => [
  226.                     new NotNull([
  227.                         'groups' => ['public_internal'],
  228.                         'message' => 'Seleccione el centro de trabajo.',
  229.                     ]),
  230.                 ],
  231.             ])
  232.             ->add('puestoTrabajo'EntityType::class, [
  233.                 'label' => 'Puesto de trabajo',
  234.                 'class' => TaxonomyValue::class,
  235.                 'choice_label' => 'label',
  236.                 'required' => $isPublicSignup,
  237.                 'placeholder' => 'Seleccione puesto',
  238.                 'query_builder' => function (EntityRepository $er) {
  239.                     return $er->createQueryBuilder('tv')
  240.                         ->join('tv.taxonomy''t')
  241.                         ->where('t.name = :name')
  242.                         ->setParameter('name''user_profile_puesto_trabajo')
  243.                         ->orderBy('tv.label''ASC');
  244.                 },
  245.                 'disabled' => $allDisabled,
  246.                 'attr' => [
  247.                     'class' => 'no-selectpicker',
  248.                 ],
  249.                 'constraints' => [
  250.                     new NotNull([
  251.                         'groups' => ['public_internal'],
  252.                         'message' => 'Seleccione el puesto de trabajo.',
  253.                     ]),
  254.                 ],
  255.             ])
  256.             ->add('especialidad'EntityType::class, [
  257.                 'label' => 'Especialidad',
  258.                 'class' => TaxonomyValue::class,
  259.                 'choice_label' => 'label',
  260.                 'required' => $isPublicSignup,
  261.                 'placeholder' => 'Seleccione especialidad',
  262.                 'query_builder' => function (EntityRepository $er) {
  263.                     return $er->createQueryBuilder('tv')
  264.                         ->join('tv.taxonomy''t')
  265.                         ->where('t.name = :name')
  266.                         ->setParameter('name''user_profile_especialidad')
  267.                         ->orderBy('tv.label''ASC');
  268.                 },
  269.                 'disabled' => $allDisabled,
  270.                 'attr' => [
  271.                     'class' => 'no-selectpicker',
  272.                 ],
  273.                 'constraints' => [
  274.                     new NotNull([
  275.                         'groups' => ['public_internal'],
  276.                         'message' => 'Seleccione la especialidad.',
  277.                     ]),
  278.                 ],
  279.             ])
  280.             ->add('grupo'EntityType::class, [
  281.                 'label' => 'Grupo',
  282.                 'class' => TaxonomyValue::class,
  283.                 'choice_label' => 'label',
  284.                 'required' => $isPublicSignup,
  285.                 'placeholder' => 'Seleccione grupo',
  286.                 'query_builder' => function (EntityRepository $er) {
  287.                     return $er->createQueryBuilder('tv')
  288.                         ->join('tv.taxonomy''t')
  289.                         ->where('t.name = :name')
  290.                         ->setParameter('name''user_profile_grupo')
  291.                         ->orderBy('tv.label''ASC');
  292.                 },
  293.                 'disabled' => $allDisabled,
  294.                 'attr' => [
  295.                     'class' => 'no-selectpicker',
  296.                 ],
  297.                 'constraints' => [
  298.                     new NotNull([
  299.                         'groups' => ['public_internal'],
  300.                         'message' => 'Seleccione el grupo.',
  301.                     ]),
  302.                 ],
  303.             ])
  304.             ->add('situacionLaboral'EntityType::class, [
  305.                 'label' => 'Situación laboral',
  306.                 'class' => TaxonomyValue::class,
  307.                 'choice_label' => 'label',
  308.                 'required' => $isPublicSignup,
  309.                 'placeholder' => 'Seleccione situación',
  310.                 'query_builder' => function (EntityRepository $er) {
  311.                     return $er->createQueryBuilder('tv')
  312.                         ->join('tv.taxonomy''t')
  313.                         ->where('t.name = :name')
  314.                         ->setParameter('name''user_profile_situacion_laboral')
  315.                         ->orderBy('tv.label''ASC');
  316.                 },
  317.                 'disabled' => $allDisabled,
  318.                 'attr' => [
  319.                     'class' => 'no-selectpicker',
  320.                 ],
  321.                 'constraints' => [
  322.                     new NotNull([
  323.                         'groups' => ['public_internal'],
  324.                         'message' => 'Seleccione la situación laboral.',
  325.                     ]),
  326.                 ],
  327.             ]);
  328.         if ($showStudentRoles) {
  329.             $builder->add('rolesSelection'ChoiceType::class, [
  330.                 'label' => 'Roles',
  331.                 'mapped' => false,
  332.                 'required' => true,
  333.                 'multiple' => true,
  334.                 'expanded' => false,
  335.                 'choices' => [
  336.                     'Alumno' => 'ROLE_ALUMNO',
  337.                     'Docente' => 'ROLE_COURSE',
  338.                 ],
  339.                 'disabled' => $allDisabled,
  340.                 'attr' => [
  341.                     'class' => 'no-selectpicker',
  342.                 ],
  343.                 'constraints' => [
  344.                     new Count([
  345.                         'groups' => ['admin_student'],
  346.                         'min' => 1,
  347.                         'minMessage' => 'Seleccione al menos un rol.',
  348.                     ]),
  349.                 ],
  350.             ]);
  351.         }
  352.         if ($showTrainerType) {
  353.             $builder
  354.                 ->add('tipoFormador'ChoiceType::class, [
  355.                     'label' => 'Tipo de formador',
  356.                     'required' => true,
  357.                     'placeholder' => 'Seleccione una opción',
  358.                     'choices' => [
  359.                         'Formador interno' => UserProfile::TIPO_FORMADOR_INTERNO,
  360.                         'Empresa externa' => UserProfile::TIPO_FORMADOR_EMPRESA_EXTERNA,
  361.                     ],
  362.                     'disabled' => $allDisabled,
  363.                     'attr' => [
  364.                         'class' => 'no-selectpicker',
  365.                     ],
  366.                     'constraints' => [
  367.                         new NotBlank([
  368.                             'groups' => ['admin_courseuser'],
  369.                             'message' => 'Seleccione el tipo de formador.',
  370.                         ]),
  371.                     ],
  372.                 ])
  373.                 ->add('entidadExterna'TextType::class, [
  374.                     'label' => 'Entidad',
  375.                     'required' => false,
  376.                     'disabled' => $allDisabled,
  377.                 ])
  378.                 ->add('cifEntidadExterna'TextType::class, [
  379.                     'label' => 'CIF',
  380.                     'required' => false,
  381.                     'disabled' => $allDisabled,
  382.                 ])
  383.                 ->add('direccionEntidadExterna'TextType::class, [
  384.                     'label' => 'Dirección de la entidad',
  385.                     'required' => false,
  386.                     'disabled' => $allDisabled,
  387.                 ])
  388.                 ->add('contactoEntidadExterna'TextType::class, [
  389.                     'label' => 'Datos de contacto de la entidad',
  390.                     'required' => false,
  391.                     'disabled' => $allDisabled,
  392.                 ]);
  393.         }
  394.         $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($isPublicSignup$allDisabled): void {
  395.             $data $event->getData();
  396.             if (!is_array($data) || !isset($data['dni'])) {
  397.                 return;
  398.             }
  399.             $profile $event->getForm()->getData();
  400.             $originalProfile $profile instanceof UserProfile $profile null;
  401.             $data['dni'] = strtoupper(preg_replace('/[^a-zA-Z0-9]/'''$data['dni']));
  402.             $normalizedTipoVia $this->resolveTipoViaOption($data['tipoVia'] ?? null);
  403.             if ($normalizedTipoVia !== null) {
  404.                 $data['tipoVia'] = $normalizedTipoVia;
  405.             }
  406.             $normalizedProvince $this->addressCatalogService->resolveProvinceName($data['provincia'] ?? null);
  407.             if ($normalizedProvince !== null) {
  408.                 $data['provincia'] = $normalizedProvince;
  409.             }
  410.             $submittedProvince = isset($data['provincia']) ? trim((string) $data['provincia']) : null;
  411.             $submittedMunicipality = isset($data['municipio']) ? trim((string) $data['municipio']) : null;
  412.             $provinceCode $this->addressCatalogService->findProvinceCodeByName($submittedProvince);
  413.             if ($provinceCode !== null) {
  414.                 $normalizedMunicipality $this->addressCatalogService->resolveMunicipalityName($provinceCode$submittedMunicipality);
  415.                 if ($normalizedMunicipality !== null) {
  416.                     $data['municipio'] = $normalizedMunicipality;
  417.                 } elseif (!$this->shouldPreserveLegacyChoice($submittedMunicipality$originalProfile?->getMunicipio(), true)
  418.                     || !$this->isSameProvinceSelection($submittedProvince$originalProfile?->getProvincia())) {
  419.                     $data['municipio'] = null;
  420.                 }
  421.             } elseif ($this->isBlankValue($submittedProvince)) {
  422.                 $data['municipio'] = null;
  423.             }
  424.             $this->addTipoViaField(
  425.                 $event->getForm(),
  426.                 $isPublicSignup,
  427.                 $allDisabled,
  428.                 isset($data['tipoVia']) ? trim((string) $data['tipoVia']) : null,
  429.                 $originalProfile?->getTipoVia(),
  430.                 true
  431.             );
  432.             $this->addProvinceField(
  433.                 $event->getForm(),
  434.                 $isPublicSignup,
  435.                 $allDisabled,
  436.                 $submittedProvince,
  437.                 $originalProfile?->getProvincia(),
  438.                 true
  439.             );
  440.             $this->addMunicipalityField(
  441.                 $event->getForm(),
  442.                 $isPublicSignup,
  443.                 $allDisabled,
  444.                 $submittedProvince,
  445.                 isset($data['municipio']) ? trim((string) $data['municipio']) : null,
  446.                 $originalProfile?->getProvincia(),
  447.                 $originalProfile?->getMunicipio(),
  448.                 true
  449.             );
  450.             $event->setData($data);
  451.         });
  452.         $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event): void {
  453.             $profile $event->getData();
  454.             if (!$profile instanceof UserProfile) {
  455.                 return;
  456.             }
  457.             if ($profile->isEmpresaExterna()) {
  458.                 $profile->clearLaborData();
  459.             }
  460.             if ($profile->isFormadorInterno()) {
  461.                 $profile->clearExternalCompanyData();
  462.             }
  463.         });
  464.     }
  465.     public function configureOptions(OptionsResolver $resolver): void
  466.     {
  467.         $resolver->setDefaults([
  468.             'data_class' => UserProfile::class,
  469.             'mode' => 'view',
  470.             'profile_context' => 'admin_courseuser',
  471.         ]);
  472.         $resolver->setAllowedValues('mode', ['alta''edit''view']);
  473.         $resolver->setAllowedValues('profile_context', ['admin_courseuser''admin_student''public_signup''public_profile']);
  474.     }
  475.     private function addTipoViaField(
  476.         FormBuilderInterface|FormInterface $form,
  477.         bool $isPublicSignup,
  478.         bool $allDisabled,
  479.         ?string $selectedValue,
  480.         ?string $originalValue,
  481.         bool $isSubmitted false
  482.     ): void {
  483.         $choices array_combine(
  484.             $this->addressCatalogService->getTipoViaOptions(),
  485.             $this->addressCatalogService->getTipoViaOptions()
  486.         );
  487.         if (!is_array($choices)) {
  488.             $choices = [];
  489.         }
  490.         $resolvedValue $this->resolveTipoViaOption($selectedValue);
  491.         if ($resolvedValue !== null) {
  492.             $selectedValue $resolvedValue;
  493.         } elseif ($this->shouldPreserveLegacyChoice($selectedValue$originalValue$isSubmitted)) {
  494.             $choices[$this->buildLegacyChoiceLabel($selectedValue)] = $selectedValue;
  495.         }
  496.         $options = [
  497.             'label' => 'Tipo de vía',
  498.             'required' => $isPublicSignup,
  499.             'disabled' => $allDisabled,
  500.             'placeholder' => 'Seleccione tipo de vía',
  501.             'choices' => $choices,
  502.             'constraints' => [
  503.                 new NotBlank([
  504.                     'groups' => ['public_signup'],
  505.                     'message' => 'El tipo de vía es obligatorio.',
  506.                 ]),
  507.             ],
  508.             'attr' => [
  509.                 'class' => 'no-selectpicker',
  510.                 'data-address-role' => 'tipo-via',
  511.             ],
  512.         ];
  513.         if (!$isSubmitted && $resolvedValue !== null && $selectedValue !== $originalValue) {
  514.             $options['data'] = $resolvedValue;
  515.         }
  516.         $form->add('tipoVia'ChoiceType::class, $options);
  517.     }
  518.     private function addProvinceField(
  519.         FormBuilderInterface|FormInterface $form,
  520.         bool $isPublicSignup,
  521.         bool $allDisabled,
  522.         ?string $selectedValue,
  523.         ?string $originalValue,
  524.         bool $isSubmitted false
  525.     ): void {
  526.         $choices $this->addressCatalogService->getProvinceChoiceMap();
  527.         $resolvedValue $this->addressCatalogService->resolveProvinceName($selectedValue);
  528.         if ($resolvedValue !== null) {
  529.             $selectedValue $resolvedValue;
  530.         } elseif ($this->shouldPreserveLegacyChoice($selectedValue$originalValue$isSubmitted)) {
  531.             $choices[$this->buildLegacyChoiceLabel($selectedValue)] = $selectedValue;
  532.         }
  533.         $options = [
  534.             'label' => 'Provincia',
  535.             'required' => $isPublicSignup,
  536.             'disabled' => $allDisabled,
  537.             'placeholder' => 'Seleccione provincia',
  538.             'choices' => $choices,
  539.             'constraints' => [
  540.                 new NotBlank([
  541.                     'groups' => ['public_signup'],
  542.                     'message' => 'La provincia es obligatoria.',
  543.                 ]),
  544.             ],
  545.             'attr' => [
  546.                 'class' => 'no-selectpicker',
  547.                 'data-address-role' => 'province',
  548.                 'data-address-municipalities-url' => $this->urlGenerator->generate('app_address_catalog_municipalities'),
  549.             ],
  550.             'choice_attr' => function (mixed $choice): array {
  551.                 $code $this->addressCatalogService->findProvinceCodeByName(is_string($choice) ? $choice null);
  552.                 return $code !== null ? ['data-address-code' => $code] : [];
  553.             },
  554.         ];
  555.         if (!$isSubmitted && $resolvedValue !== null && $selectedValue !== $originalValue) {
  556.             $options['data'] = $resolvedValue;
  557.         }
  558.         $form->add('provincia'ChoiceType::class, $options);
  559.     }
  560.     private function addMunicipalityField(
  561.         FormBuilderInterface|FormInterface $form,
  562.         bool $isPublicSignup,
  563.         bool $allDisabled,
  564.         ?string $provinceValue,
  565.         ?string $selectedValue,
  566.         ?string $originalProvinceValue,
  567.         ?string $originalValue,
  568.         bool $isSubmitted false
  569.     ): void {
  570.         $provinceCode $this->addressCatalogService->findProvinceCodeByName($provinceValue);
  571.         $choices $provinceCode !== null $this->addressCatalogService->getMunicipalityChoiceMap($provinceCode) : [];
  572.         $resolvedValue $provinceCode !== null
  573.             $this->addressCatalogService->resolveMunicipalityName($provinceCode$selectedValue)
  574.             : null;
  575.         if ($resolvedValue !== null) {
  576.             $selectedValue $resolvedValue;
  577.         } elseif (
  578.             $this->isSameProvinceSelection($provinceValue$originalProvinceValue)
  579.             && $this->shouldPreserveLegacyChoice($selectedValue$originalValue$isSubmitted)
  580.         ) {
  581.             $choices[$this->buildLegacyChoiceLabel($selectedValue)] = $selectedValue;
  582.         }
  583.         $options = [
  584.             'label' => 'Municipio',
  585.             'required' => $isPublicSignup,
  586.             'disabled' => $allDisabled,
  587.             'placeholder' => $provinceCode === null 'Seleccione provincia primero' 'Seleccione municipio',
  588.             'choices' => $choices,
  589.             'constraints' => [
  590.                 new NotBlank([
  591.                     'groups' => ['public_signup'],
  592.                     'message' => 'El municipio es obligatorio.',
  593.                 ]),
  594.             ],
  595.             'attr' => [
  596.                 'class' => 'no-selectpicker',
  597.                 'data-address-role' => 'municipality',
  598.                 'data-address-placeholder-empty' => 'Seleccione provincia primero',
  599.                 'data-address-placeholder-ready' => 'Seleccione municipio',
  600.             ],
  601.             'choice_attr' => function (mixed $choice) use ($provinceCode): array {
  602.                 if (!is_string($choice) || $provinceCode === null) {
  603.                     return [];
  604.                 }
  605.                 $code $this->addressCatalogService->findMunicipalityCodeByName($provinceCode$choice);
  606.                 return $code !== null ? ['data-address-code' => $code] : [];
  607.             },
  608.         ];
  609.         if (!$allDisabled && $provinceCode === null && $this->isBlankValue($selectedValue)) {
  610.             $options['attr']['disabled'] = 'disabled';
  611.         }
  612.         if (!$isSubmitted && $resolvedValue !== null && $selectedValue !== $originalValue) {
  613.             $options['data'] = $resolvedValue;
  614.         }
  615.         $form->add('municipio'ChoiceType::class, $options);
  616.     }
  617.     private function resolveTipoViaOption(?string $value): ?string
  618.     {
  619.         if ($value === null || trim($value) === '') {
  620.             return null;
  621.         }
  622.         $normalizedValue mb_strtoupper(trim($value));
  623.         foreach ($this->addressCatalogService->getTipoViaOptions() as $option) {
  624.             if ($normalizedValue === $option) {
  625.                 return $option;
  626.             }
  627.         }
  628.         return null;
  629.     }
  630.     private function shouldPreserveLegacyChoice(?string $selectedValue, ?string $originalValuebool $isSubmitted): bool
  631.     {
  632.         if ($selectedValue === null || trim($selectedValue) === '') {
  633.             return false;
  634.         }
  635.         if ($originalValue === null || trim($originalValue) === '') {
  636.             return !$isSubmitted;
  637.         }
  638.         if (!$isSubmitted) {
  639.             return true;
  640.         }
  641.         return trim($selectedValue) === trim($originalValue);
  642.     }
  643.     private function isSameProvinceSelection(?string $provinceValue, ?string $originalProvinceValue): bool
  644.     {
  645.         if ($provinceValue === null || $originalProvinceValue === null) {
  646.             return $provinceValue === $originalProvinceValue;
  647.         }
  648.         if (trim($provinceValue) === trim($originalProvinceValue)) {
  649.             return true;
  650.         }
  651.         $currentProvinceCode $this->addressCatalogService->findProvinceCodeByName($provinceValue);
  652.         $originalProvinceCode $this->addressCatalogService->findProvinceCodeByName($originalProvinceValue);
  653.         return $currentProvinceCode !== null
  654.             && $originalProvinceCode !== null
  655.             && $currentProvinceCode === $originalProvinceCode;
  656.     }
  657.     private function buildLegacyChoiceLabel(?string $value): string
  658.     {
  659.         return sprintf('%s (valor actual no estandarizado)'trim((string) $value));
  660.     }
  661.     private function isBlankValue(?string $value): bool
  662.     {
  663.         return $value === null || trim($value) === '';
  664.     }
  665. }