src/Form/UserProfileType.php line 30
<?phpnamespace App\Form;use App\Entity\Centers;use App\Entity\User;use App\Entity\UserProfile;use App\Entity\TaxonomyValue;use App\Services\AddressCatalogService;use App\Services\RoleAccessService;use Symfony\Component\Form\AbstractType;use Symfony\Component\Form\FormBuilderInterface;use Symfony\Component\Form\Extension\Core\Type\ChoiceType;use Symfony\Component\Form\Extension\Core\Type\DateType;use Symfony\Component\Form\Extension\Core\Type\TextType;use Symfony\Bridge\Doctrine\Form\Type\EntityType;use Symfony\Component\OptionsResolver\OptionsResolver;use Doctrine\ORM\EntityRepository;use Symfony\Component\Form\Extension\Core\Type\EmailType;use Symfony\Component\Form\FormEvent;use Symfony\Component\Form\FormEvents;use Symfony\Component\Form\FormInterface;use Symfony\Component\Validator\Constraints\Count;use Symfony\Component\Validator\Constraints\NotBlank;use Symfony\Component\Validator\Constraints\NotNull;use Symfony\Component\Validator\Constraints\Regex;use Symfony\Component\Security\Core\Security;use Symfony\Component\Routing\Generator\UrlGeneratorInterface;class UserProfileType extends AbstractType{public function __construct(private Security $security,private RoleAccessService $roleAccessService,private AddressCatalogService $addressCatalogService,private UrlGeneratorInterface $urlGenerator) {}public function buildForm(FormBuilderInterface $builder, array $options): void{$mode = $options['mode']; // alta | edit | view$profileContext = $options['profile_context'];$isPublicSignup = $profileContext === 'public_signup';$showTrainerType = $profileContext === 'admin_courseuser';$showStudentRoles = $profileContext === 'admin_student';$actor = $this->security->getUser();$canManageTrainerCenter = $showTrainerType && ($this->security->isGranted('ROLE_SUPERUSER') || $this->security->isGranted('ROLE_CENTER'));$originalProfile = $builder->getData() instanceof UserProfile ? $builder->getData() : null;$allDisabled = $mode === 'view';$builder// DNI->add('dni', TextType::class, ['mapped' => false,'disabled' => $mode !== 'alta', // SOLO editable en alta'constraints' => [new NotBlank(['message' => 'El DNI/NIE es obligatorio.']),new Regex(['pattern' => '/^(?:\\d{8}|[XYZ]\\d{7})[A-Z]$/i','message' => 'El DNI/NIE debe tener el formato correcto.',]),],])// EMAIL->add('email', EmailType::class, ['mapped' => false,'disabled' => $mode !== 'alta', // SOLO editable en alta]);if ($canManageTrainerCenter) {$builder->add('managementCenter', EntityType::class, ['mapped' => false,'class' => Centers::class,'choice_label' => 'Name','label' => 'Centro de gestión','required' => false,'choices' => $actor instanceof User ? $this->roleAccessService->getAssignableCenters($actor) : [],'placeholder' => 'Seleccione centro','disabled' => $allDisabled,'attr' => ['class' => 'no-selectpicker',],]);}$builder/* ================= DATOS PERSONALES ================= */->add('nombre', TextType::class, ['label' => 'Nombre','disabled' => $allDisabled,])->add('apellido1', TextType::class, ['label' => 'Primer apellido','disabled' => $allDisabled,])->add('apellido2', TextType::class, ['label' => 'Segundo apellido','disabled' => $allDisabled,])->add('fechaNacimiento', DateType::class, ['label' => 'Fecha de nacimiento','widget' => 'single_text','disabled' => $allDisabled,])->add('sexo', ChoiceType::class, ['label' => 'Sexo (*)','required' => true,'placeholder' => 'Seleccione sexo','choices' => ['Hombre' => 'H','Mujer' => 'M',],'disabled' => $allDisabled,'attr' => ['class' => 'no-selectpicker',],]);$this->addTipoViaField($builder,$isPublicSignup,$allDisabled,$originalProfile?->getTipoVia(),$originalProfile?->getTipoVia());$this->addProvinceField($builder,$isPublicSignup,$allDisabled,$originalProfile?->getProvincia(),$originalProfile?->getProvincia());$this->addMunicipalityField($builder,$isPublicSignup,$allDisabled,$originalProfile?->getProvincia(),$originalProfile?->getMunicipio(),$originalProfile?->getProvincia(),$originalProfile?->getMunicipio());$builder/* ================= DIRECCIÓN ================= */->add('nombreVia', TextType::class, ['label' => 'Nombre de la vía','required' => $isPublicSignup,'disabled' => $allDisabled,'constraints' => [new NotBlank(['groups' => ['public_signup'],'message' => 'El nombre de la vía es obligatorio.',]),],])->add('numeroVia', TextType::class, ['label' => 'Número','required' => $isPublicSignup,'disabled' => $allDisabled,'constraints' => [new NotBlank(['groups' => ['public_signup'],'message' => 'El número es obligatorio.',]),],])->add('codigoPostal', TextType::class, ['label' => 'Código postal','required' => $isPublicSignup,'disabled' => $allDisabled,'constraints' => [new NotBlank(['groups' => ['public_signup'],'message' => 'El código postal es obligatorio.',]),],])->add('portal', TextType::class, ['label' => 'Portal','required' => false,'disabled' => $allDisabled,])->add('bloque', TextType::class, ['label' => 'Bloque','required' => false,'disabled' => $allDisabled,])->add('escalera', TextType::class, ['label' => 'Escalera','required' => false,'disabled' => $allDisabled,])->add('planta', TextType::class, ['label' => 'Planta','required' => false,'disabled' => $allDisabled,])->add('puerta', TextType::class, ['label' => 'Puerta','required' => false,'disabled' => $allDisabled,])->add('telefono', TextType::class, ['label' => 'Teléfono móvil','required' => $isPublicSignup,'disabled' => $allDisabled,'constraints' => [new NotBlank(['groups' => ['public_signup'],'message' => 'El teléfono móvil es obligatorio.',]),],])->add('centroTrabajo', EntityType::class, ['label' => 'Centro de trabajo','class' => TaxonomyValue::class,'choice_label' => 'label','required' => $isPublicSignup,'placeholder' => 'Seleccione centro','query_builder' => function (EntityRepository $er) {return $er->createQueryBuilder('tv')->join('tv.taxonomy', 't')->where('t.name = :name')->setParameter('name', 'user_profile_centro_trabajo')->orderBy('tv.label', 'ASC');},'disabled' => $allDisabled,'attr' => ['class' => 'no-selectpicker',],'constraints' => [new NotNull(['groups' => ['public_internal'],'message' => 'Seleccione el centro de trabajo.',]),],])->add('puestoTrabajo', EntityType::class, ['label' => 'Puesto de trabajo','class' => TaxonomyValue::class,'choice_label' => 'label','required' => $isPublicSignup,'placeholder' => 'Seleccione puesto','query_builder' => function (EntityRepository $er) {return $er->createQueryBuilder('tv')->join('tv.taxonomy', 't')->where('t.name = :name')->setParameter('name', 'user_profile_puesto_trabajo')->orderBy('tv.label', 'ASC');},'disabled' => $allDisabled,'attr' => ['class' => 'no-selectpicker',],'constraints' => [new NotNull(['groups' => ['public_internal'],'message' => 'Seleccione el puesto de trabajo.',]),],])->add('especialidad', EntityType::class, ['label' => 'Especialidad','class' => TaxonomyValue::class,'choice_label' => 'label','required' => $isPublicSignup,'placeholder' => 'Seleccione especialidad','query_builder' => function (EntityRepository $er) {return $er->createQueryBuilder('tv')->join('tv.taxonomy', 't')->where('t.name = :name')->setParameter('name', 'user_profile_especialidad')->orderBy('tv.label', 'ASC');},'disabled' => $allDisabled,'attr' => ['class' => 'no-selectpicker',],'constraints' => [new NotNull(['groups' => ['public_internal'],'message' => 'Seleccione la especialidad.',]),],])->add('grupo', EntityType::class, ['label' => 'Grupo','class' => TaxonomyValue::class,'choice_label' => 'label','required' => $isPublicSignup,'placeholder' => 'Seleccione grupo','query_builder' => function (EntityRepository $er) {return $er->createQueryBuilder('tv')->join('tv.taxonomy', 't')->where('t.name = :name')->setParameter('name', 'user_profile_grupo')->orderBy('tv.label', 'ASC');},'disabled' => $allDisabled,'attr' => ['class' => 'no-selectpicker',],'constraints' => [new NotNull(['groups' => ['public_internal'],'message' => 'Seleccione el grupo.',]),],])->add('situacionLaboral', EntityType::class, ['label' => 'Situación laboral','class' => TaxonomyValue::class,'choice_label' => 'label','required' => $isPublicSignup,'placeholder' => 'Seleccione situación','query_builder' => function (EntityRepository $er) {return $er->createQueryBuilder('tv')->join('tv.taxonomy', 't')->where('t.name = :name')->setParameter('name', 'user_profile_situacion_laboral')->orderBy('tv.label', 'ASC');},'disabled' => $allDisabled,'attr' => ['class' => 'no-selectpicker',],'constraints' => [new NotNull(['groups' => ['public_internal'],'message' => 'Seleccione la situación laboral.',]),],]);if ($showStudentRoles) {$builder->add('rolesSelection', ChoiceType::class, ['label' => 'Roles','mapped' => false,'required' => true,'multiple' => true,'expanded' => false,'choices' => ['Alumno' => 'ROLE_ALUMNO','Docente' => 'ROLE_COURSE',],'disabled' => $allDisabled,'attr' => ['class' => 'no-selectpicker',],'constraints' => [new Count(['groups' => ['admin_student'],'min' => 1,'minMessage' => 'Seleccione al menos un rol.',]),],]);}if ($showTrainerType) {$builder->add('tipoFormador', ChoiceType::class, ['label' => 'Tipo de formador','required' => true,'placeholder' => 'Seleccione una opción','choices' => ['Formador interno' => UserProfile::TIPO_FORMADOR_INTERNO,'Empresa externa' => UserProfile::TIPO_FORMADOR_EMPRESA_EXTERNA,],'disabled' => $allDisabled,'attr' => ['class' => 'no-selectpicker',],'constraints' => [new NotBlank(['groups' => ['admin_courseuser'],'message' => 'Seleccione el tipo de formador.',]),],])->add('entidadExterna', TextType::class, ['label' => 'Entidad','required' => false,'disabled' => $allDisabled,])->add('cifEntidadExterna', TextType::class, ['label' => 'CIF','required' => false,'disabled' => $allDisabled,])->add('direccionEntidadExterna', TextType::class, ['label' => 'Dirección de la entidad','required' => false,'disabled' => $allDisabled,])->add('contactoEntidadExterna', TextType::class, ['label' => 'Datos de contacto de la entidad','required' => false,'disabled' => $allDisabled,]);}$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($isPublicSignup, $allDisabled): void {$data = $event->getData();if (!is_array($data) || !isset($data['dni'])) {return;}$profile = $event->getForm()->getData();$originalProfile = $profile instanceof UserProfile ? $profile : null;$data['dni'] = strtoupper(preg_replace('/[^a-zA-Z0-9]/', '', $data['dni']));$normalizedTipoVia = $this->resolveTipoViaOption($data['tipoVia'] ?? null);if ($normalizedTipoVia !== null) {$data['tipoVia'] = $normalizedTipoVia;}$normalizedProvince = $this->addressCatalogService->resolveProvinceName($data['provincia'] ?? null);if ($normalizedProvince !== null) {$data['provincia'] = $normalizedProvince;}$submittedProvince = isset($data['provincia']) ? trim((string) $data['provincia']) : null;$submittedMunicipality = isset($data['municipio']) ? trim((string) $data['municipio']) : null;$provinceCode = $this->addressCatalogService->findProvinceCodeByName($submittedProvince);if ($provinceCode !== null) {$normalizedMunicipality = $this->addressCatalogService->resolveMunicipalityName($provinceCode, $submittedMunicipality);if ($normalizedMunicipality !== null) {$data['municipio'] = $normalizedMunicipality;} elseif (!$this->shouldPreserveLegacyChoice($submittedMunicipality, $originalProfile?->getMunicipio(), true)|| !$this->isSameProvinceSelection($submittedProvince, $originalProfile?->getProvincia())) {$data['municipio'] = null;}} elseif ($this->isBlankValue($submittedProvince)) {$data['municipio'] = null;}$this->addTipoViaField($event->getForm(),$isPublicSignup,$allDisabled,isset($data['tipoVia']) ? trim((string) $data['tipoVia']) : null,$originalProfile?->getTipoVia(),true);$this->addProvinceField($event->getForm(),$isPublicSignup,$allDisabled,$submittedProvince,$originalProfile?->getProvincia(),true);$this->addMunicipalityField($event->getForm(),$isPublicSignup,$allDisabled,$submittedProvince,isset($data['municipio']) ? trim((string) $data['municipio']) : null,$originalProfile?->getProvincia(),$originalProfile?->getMunicipio(),true);$event->setData($data);});$builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event): void {$profile = $event->getData();if (!$profile instanceof UserProfile) {return;}if ($profile->isEmpresaExterna()) {$profile->clearLaborData();}if ($profile->isFormadorInterno()) {$profile->clearExternalCompanyData();}});}public function configureOptions(OptionsResolver $resolver): void{$resolver->setDefaults(['data_class' => UserProfile::class,'mode' => 'view','profile_context' => 'admin_courseuser',]);$resolver->setAllowedValues('mode', ['alta', 'edit', 'view']);$resolver->setAllowedValues('profile_context', ['admin_courseuser', 'admin_student', 'public_signup', 'public_profile']);}private function addTipoViaField(FormBuilderInterface|FormInterface $form,bool $isPublicSignup,bool $allDisabled,?string $selectedValue,?string $originalValue,bool $isSubmitted = false): void {$choices = array_combine($this->addressCatalogService->getTipoViaOptions(),$this->addressCatalogService->getTipoViaOptions());if (!is_array($choices)) {$choices = [];}$resolvedValue = $this->resolveTipoViaOption($selectedValue);if ($resolvedValue !== null) {$selectedValue = $resolvedValue;} elseif ($this->shouldPreserveLegacyChoice($selectedValue, $originalValue, $isSubmitted)) {$choices[$this->buildLegacyChoiceLabel($selectedValue)] = $selectedValue;}$options = ['label' => 'Tipo de vía','required' => $isPublicSignup,'disabled' => $allDisabled,'placeholder' => 'Seleccione tipo de vía','choices' => $choices,'constraints' => [new NotBlank(['groups' => ['public_signup'],'message' => 'El tipo de vía es obligatorio.',]),],'attr' => ['class' => 'no-selectpicker','data-address-role' => 'tipo-via',],];if (!$isSubmitted && $resolvedValue !== null && $selectedValue !== $originalValue) {$options['data'] = $resolvedValue;}$form->add('tipoVia', ChoiceType::class, $options);}private function addProvinceField(FormBuilderInterface|FormInterface $form,bool $isPublicSignup,bool $allDisabled,?string $selectedValue,?string $originalValue,bool $isSubmitted = false): void {$choices = $this->addressCatalogService->getProvinceChoiceMap();$resolvedValue = $this->addressCatalogService->resolveProvinceName($selectedValue);if ($resolvedValue !== null) {$selectedValue = $resolvedValue;} elseif ($this->shouldPreserveLegacyChoice($selectedValue, $originalValue, $isSubmitted)) {$choices[$this->buildLegacyChoiceLabel($selectedValue)] = $selectedValue;}$options = ['label' => 'Provincia','required' => $isPublicSignup,'disabled' => $allDisabled,'placeholder' => 'Seleccione provincia','choices' => $choices,'constraints' => [new NotBlank(['groups' => ['public_signup'],'message' => 'La provincia es obligatoria.',]),],'attr' => ['class' => 'no-selectpicker','data-address-role' => 'province','data-address-municipalities-url' => $this->urlGenerator->generate('app_address_catalog_municipalities'),],'choice_attr' => function (mixed $choice): array {$code = $this->addressCatalogService->findProvinceCodeByName(is_string($choice) ? $choice : null);return $code !== null ? ['data-address-code' => $code] : [];},];if (!$isSubmitted && $resolvedValue !== null && $selectedValue !== $originalValue) {$options['data'] = $resolvedValue;}$form->add('provincia', ChoiceType::class, $options);}private function addMunicipalityField(FormBuilderInterface|FormInterface $form,bool $isPublicSignup,bool $allDisabled,?string $provinceValue,?string $selectedValue,?string $originalProvinceValue,?string $originalValue,bool $isSubmitted = false): void {$provinceCode = $this->addressCatalogService->findProvinceCodeByName($provinceValue);$choices = $provinceCode !== null ? $this->addressCatalogService->getMunicipalityChoiceMap($provinceCode) : [];$resolvedValue = $provinceCode !== null? $this->addressCatalogService->resolveMunicipalityName($provinceCode, $selectedValue): null;if ($resolvedValue !== null) {$selectedValue = $resolvedValue;} elseif ($this->isSameProvinceSelection($provinceValue, $originalProvinceValue)&& $this->shouldPreserveLegacyChoice($selectedValue, $originalValue, $isSubmitted)) {$choices[$this->buildLegacyChoiceLabel($selectedValue)] = $selectedValue;}$options = ['label' => 'Municipio','required' => $isPublicSignup,'disabled' => $allDisabled,'placeholder' => $provinceCode === null ? 'Seleccione provincia primero' : 'Seleccione municipio','choices' => $choices,'constraints' => [new NotBlank(['groups' => ['public_signup'],'message' => 'El municipio es obligatorio.',]),],'attr' => ['class' => 'no-selectpicker','data-address-role' => 'municipality','data-address-placeholder-empty' => 'Seleccione provincia primero','data-address-placeholder-ready' => 'Seleccione municipio',],'choice_attr' => function (mixed $choice) use ($provinceCode): array {if (!is_string($choice) || $provinceCode === null) {return [];}$code = $this->addressCatalogService->findMunicipalityCodeByName($provinceCode, $choice);return $code !== null ? ['data-address-code' => $code] : [];},];if (!$allDisabled && $provinceCode === null && $this->isBlankValue($selectedValue)) {$options['attr']['disabled'] = 'disabled';}if (!$isSubmitted && $resolvedValue !== null && $selectedValue !== $originalValue) {$options['data'] = $resolvedValue;}$form->add('municipio', ChoiceType::class, $options);}private function resolveTipoViaOption(?string $value): ?string{if ($value === null || trim($value) === '') {return null;}$normalizedValue = mb_strtoupper(trim($value));foreach ($this->addressCatalogService->getTipoViaOptions() as $option) {if ($normalizedValue === $option) {return $option;}}return null;}private function shouldPreserveLegacyChoice(?string $selectedValue, ?string $originalValue, bool $isSubmitted): bool{if ($selectedValue === null || trim($selectedValue) === '') {return false;}if ($originalValue === null || trim($originalValue) === '') {return !$isSubmitted;}if (!$isSubmitted) {return true;}return trim($selectedValue) === trim($originalValue);}private function isSameProvinceSelection(?string $provinceValue, ?string $originalProvinceValue): bool{if ($provinceValue === null || $originalProvinceValue === null) {return $provinceValue === $originalProvinceValue;}if (trim($provinceValue) === trim($originalProvinceValue)) {return true;}$currentProvinceCode = $this->addressCatalogService->findProvinceCodeByName($provinceValue);$originalProvinceCode = $this->addressCatalogService->findProvinceCodeByName($originalProvinceValue);return $currentProvinceCode !== null&& $originalProvinceCode !== null&& $currentProvinceCode === $originalProvinceCode;}private function buildLegacyChoiceLabel(?string $value): string{return sprintf('%s (valor actual no estandarizado)', trim((string) $value));}private function isBlankValue(?string $value): bool{return $value === null || trim($value) === '';}}