src/Service/JsonFormManager.php line 32

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use App\Entity\Contracts\FormInterface;
  4. use App\Entity\Form;
  5. use App\Entity\FormLeadData;
  6. use App\Entity\MasterForm;
  7. use App\Form\CollectionJsonType;
  8. use App\Validator\ExclusiveChoices;
  9. use App\Validator\UniqueLeadDataBy;
  10. use DateTime;
  11. use Exception;
  12. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  13. use Symfony\Component\Filesystem\Exception\FileNotFoundException;
  14. use Symfony\Component\Filesystem\Filesystem;
  15. use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
  16. use Symfony\Component\Form\Extension\Core\Type\CollectionType;
  17. use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
  18. use Symfony\Component\Form\Extension\Core\Type\DateType;
  19. use Symfony\Component\Form\Extension\Core\Type\EmailType;
  20. use Symfony\Component\Form\Extension\Core\Type\FormType;
  21. use Symfony\Component\Form\Extension\Core\Type\HiddenType;
  22. use Symfony\Component\Form\Extension\Core\Type\NumberType;
  23. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  24. use Symfony\Component\Form\Extension\Core\Type\TextareaType;
  25. use Symfony\Component\Form\Extension\Core\Type\TextType;
  26. use Symfony\Component\Validator\Constraints\Email;
  27. use Symfony\Component\Validator\Constraints\NotBlank;
  28. use Symfony\Component\Validator\Validator\ValidatorInterface;
  29. class JsonFormManager
  30. {
  31.     /** @var Filesystem $filesystem */
  32.     private $filesystem;
  33.     /** @var array $formDefinition */
  34.     private $formDefinition;
  35.     /** @var array|null Definición ya construida; se invalida al cambiar form, query params o fichero */
  36.     private $builtFormDefinition;
  37.     /** @var ParameterBagInterface $params */
  38.     private $params;
  39.     /** @var ValidatorInterface $validator */
  40.     private $validator;
  41.     /** @var Form $form */
  42.     private $form;
  43.     /** @var array $queryParams */
  44.     private $queryParams = [];
  45.     public const FOLDER_NAME = 'forms';
  46.     public const GROUP_TYPE = 'group';
  47.     public const TEXT_TYPE = 'text';
  48.     public const TEXTAREA_TYPE = 'textarea';
  49.     public const EMAIL_TYPE = 'email';
  50.     public const NUMBER_TYPE = 'number';
  51.     public const SELECT_TYPE = 'select';
  52.     public const CHECKBOX_TYPE = 'checkbox';
  53.     public const RADIO_TYPE = 'radio';
  54.     public const DATE_TYPE = 'date';
  55.     public const DATETIME_TYPE = 'datetime';
  56.     public const HIDDEN_TYPE = 'hidden';
  57.     public const COLLECTION_TYPE = 'collection';
  58.     public const SUBMIT_TYPE = 'submit';
  59.     public const SUPPORTED_TYPES = [
  60.         self::GROUP_TYPE,
  61.         self::TEXT_TYPE,
  62.         self::TEXTAREA_TYPE,
  63.         self::EMAIL_TYPE,
  64.         self::NUMBER_TYPE,
  65.         self::SELECT_TYPE,
  66.         self::CHECKBOX_TYPE,
  67.         self::RADIO_TYPE,
  68.         self::DATE_TYPE,
  69.         self::DATETIME_TYPE,
  70.         self::HIDDEN_TYPE,
  71.         self::COLLECTION_TYPE,
  72.         self::SUBMIT_TYPE
  73.     ];
  74.     public const SELECTABLE_TYPES = [
  75.         self::SELECT_TYPE,
  76.         self::CHECKBOX_TYPE,
  77.         self::RADIO_TYPE
  78.     ];
  79.     public const ADDABLE_TYPES = [
  80.         self::TEXT_TYPE => self::TEXT_TYPE,
  81.         self::TEXTAREA_TYPE => self::TEXTAREA_TYPE,
  82.         self::EMAIL_TYPE => self::EMAIL_TYPE,
  83.         self::NUMBER_TYPE => self::NUMBER_TYPE,
  84.         // TODO
  85.         // self::SELECT_TYPE => self::SELECT_TYPE,
  86.         // self::CHECKBOX_TYPE => self::CHECKBOX_TYPE,
  87.         // self::RADIO_TYPE => self::RADIO_TYPE,
  88.         self::DATE_TYPE => self::DATE_TYPE,
  89.         self::DATETIME_TYPE => self::DATETIME_TYPE,
  90.         self::HIDDEN_TYPE => self::HIDDEN_TYPE
  91.     ];
  92.     public const VALUABLE_TYPES = [
  93.         self::TEXT_TYPE => self::TEXT_TYPE,
  94.         self::TEXTAREA_TYPE => self::TEXTAREA_TYPE,
  95.         self::EMAIL_TYPE => self::EMAIL_TYPE,
  96.         self::NUMBER_TYPE => self::NUMBER_TYPE,
  97.         self::SELECT_TYPE => self::SELECT_TYPE,
  98.         self::CHECKBOX_TYPE => self::CHECKBOX_TYPE,
  99.         self::RADIO_TYPE => self::RADIO_TYPE,
  100.         self::DATE_TYPE => self::DATE_TYPE,
  101.         self::DATETIME_TYPE => self::DATETIME_TYPE,
  102.         self::HIDDEN_TYPE => self::HIDDEN_TYPE
  103.     ];
  104.     // Delimiters
  105.     public const FIELDNAME_DELIMITER = '_';
  106.     public const CHECKBOX_VIEW_DELIMITER = ' | ';
  107.     public const INCOMPATIBLE_FIELD_DELIMITER = '__';
  108.     public const COLLECTION_INDEX_DELIMITER = '#';
  109.     /** Tope defensivo de elementos por apartado repetible al expandir columnas y celdas */
  110.     public const MAX_COLLECTION_ITEMS = 50;
  111.     /**
  112.      * @param Filesystem $filesystem
  113.      * @param ParameterBagInterface $params
  114.      * @param ValidatorInterface $validator
  115.      */
  116.     public function __construct(Filesystem $filesystem, ParameterBagInterface $params, ValidatorInterface $validator)
  117.     {
  118.         $this->filesystem = $filesystem;
  119.         $this->params = $params;
  120.         $this->validator = $validator;
  121.     }
  122.     /**
  123.      * @return FormInterface|null
  124.      */
  125.     public function getForm(): ?FormInterface
  126.     {
  127.         return $this->form;
  128.     }
  129.     /**
  130.      * @param FormInterface $form
  131.      * @return self
  132.      */
  133.     public function setForm(FormInterface $form): self
  134.     {
  135.         $this->form = $form;
  136.         $this->builtFormDefinition = null;
  137.         return $this;
  138.     }
  139.     /**
  140.      * @param array $queryParams
  141.      * @return $this
  142.      */
  143.     public function setQueryParams(array $queryParams): self
  144.     {
  145.         $this->queryParams = $queryParams;
  146.         $this->builtFormDefinition = null;
  147.         return $this;
  148.     }
  149.     /**
  150.      * @param string $filePath
  151.      * @return $this
  152.      */
  153.     public function loadFromFilePath(string $filePath): JsonFormManager
  154.     {
  155.         $this->builtFormDefinition = null;
  156.         $filePath = $this->getBasePath() . "/" . $filePath;
  157.         if (!$this->filesystem->exists($filePath)) {
  158.             throw new FileNotFoundException();
  159.         }
  160.         $this->formDefinition = json_decode(file_get_contents($filePath), true);
  161.         if ($this->form instanceof MasterForm) {
  162.             return $this;
  163.         }
  164.         if ($formFieldAddings = $this->form->getFormFieldAddings()) {
  165.             foreach ($formFieldAddings as $adding) {
  166.                 // TODO group 0 only
  167.                 $this->formDefinition[0]['fields'][] = [
  168.                     'type' => $adding->getType(),
  169.                     'common' => [
  170.                         'name' => $adding->getName(),
  171.                         'label' => $adding->getLabel()
  172.                     ],
  173.                     'attr' => [],
  174.                     'constraints' => []
  175.                 ];
  176.             }
  177.         }
  178.         $this->addFieldToDefinition();
  179.         return $this;
  180.     }
  181.     public function getFromFormLead(Type $var = null)
  182.     {
  183.         # code...
  184.     }
  185.     /**
  186.      * @return array|array[]
  187.      * @throws Exception
  188.      */
  189.     public function getFormDefinition(): array
  190.     {
  191.         if (!$this->formDefinition) {
  192.             throw new Exception('No se puede obtener la definición del formulario sin cargar antes el archvo de definición');
  193.         }
  194.         if (null !== $this->builtFormDefinition) {
  195.             return $this->builtFormDefinition;
  196.         }
  197.         return $this->builtFormDefinition = array_map(function($value) {
  198.             return $this->buildFormItem($value);
  199.         }, $this->formDefinition);
  200.     }
  201.     /**
  202.      * @param array $formItem
  203.      * @return array
  204.      * @throws Exception
  205.      */
  206.     private function buildFormItem(array $formItem): array
  207.     {
  208.         if (!$this->formTypeIsSupported($formItem['type'])) {
  209.             throw new Exception(sprintf('El tipo %s no está soportado', $formItem['type']));
  210.         }
  211.         if ($formItem['type'] == self::GROUP_TYPE) {
  212.             $nativeFields = [];
  213.             foreach ($formItem['fields'] as $formSubItem) {
  214.                 $nativeFields[] = $this->buildFormItem($formSubItem);
  215.             }
  216.             return [
  217.                 'name' =>  $formItem['name'],
  218.                 'class' => FormType::class,
  219.                 'options' => [
  220.                     'label' => $formItem['title'],
  221.                     'inherit_data' => true
  222.                 ],
  223.                 'fields' => $nativeFields,
  224.                 'attr' => isset($formItem['attr']) ? $formItem['attr'] : []
  225.             ];
  226.         }
  227.         return $this->configureNativeFormItemByType($this->buildNativeFormItem($formItem), $formItem);
  228.     }
  229.     /**
  230.      * @param array $formItem
  231.      * @return array
  232.      * @throws Exception
  233.      */
  234.     private function buildNativeFormItem(array $formItem): array
  235.     {
  236.         $nativeFormItem = [
  237.             'name' =>  self::buildFieldName($formItem['common']['name'], $formItem['type']),
  238.             'options' => [
  239.                 'label' => $formItem['common']['label'],
  240.                 'attr' => $formItem['attr'],
  241.                 'label_html' => true
  242.             ]
  243.         ];
  244.         if (isset($formItem['common']['help'])) {
  245.             $nativeFormItem['options']['help'] = $formItem['common']['help'];
  246.         }
  247.         if (isset($formItem['attr']['disabled'])) {
  248.             $nativeFormItem['options']['disabled'] = $formItem['attr']['disabled'];
  249.         }
  250.         if (isset($formItem['constraints']['required'])) {
  251.             $nativeFormItem['options']['required'] = $formItem['constraints']['required'];
  252.         }
  253.         // Check if persist field
  254.         if (isset($formItem['bind'])) {
  255.             $nativeFormItem['options']['attr']['bind'] = $formItem['bind'];
  256.         }
  257.         // Check if unique
  258.         if (isset($formItem['constraints']['unique'])) {
  259.             $nativeFormItem['options']['attr']['unique'] = $formItem['constraints']['unique'];
  260.         }
  261.         if (self::COLLECTION_TYPE === $formItem['type']) {
  262.             $nativeFormItem = $this->buildNativeCollection($nativeFormItem, $formItem);
  263.         }
  264.         // Constraints
  265.         $nativeFormItem = $this->buildConstraints($nativeFormItem, $formItem);
  266.         // Opciones exclusivas: además del JS, se valida en servidor que no vengan con otras
  267.         if (!empty($formItem['attr']['clean-choices-value'])) {
  268.             $nativeFormItem['options']['constraints'][] = new ExclusiveChoices(
  269.                 explode('|', (string) $formItem['attr']['clean-choices-value'])
  270.             );
  271.         }
  272.         // Conditioned fields
  273.         return $this->getConditionedFields($nativeFormItem, $formItem);
  274.     }
  275.     /**
  276.      * @param array $nativeFormItem
  277.      * @param array $formItem
  278.      * @return array
  279.      * @throws Exception
  280.      */
  281.     private function buildNativeCollection(array $nativeFormItem, array $formItem): array
  282.     {
  283.         // Min number of elements (for type collections)
  284.         if (isset($formItem['constraints']['min_items'])) {
  285.             if (!is_numeric($formItem['constraints']['min_items'])) {
  286.                 throw new Exception($formItem['constraints']['min_items'] . ' debe ser un número entero');
  287.             }
  288.             $nativeFormItem['options']['attr']['min_items'] = $formItem['constraints']['min_items'];
  289.         }
  290.         // Max number of elements (for type collections)
  291.         if (isset($formItem['constraints']['max_items'])) {
  292.             if (!is_numeric($formItem['constraints']['max_items'])) {
  293.                 throw new Exception($formItem['constraints']['max_items'] . ' debe ser un número entero');
  294.             }
  295.             $nativeFormItem['options']['attr']['max_items'] = $formItem['constraints']['max_items'];
  296.         }
  297.         // Add label text (for type collections)
  298.         if (isset($formItem['constraints']['add_label'])) {
  299.             if (!is_string($formItem['constraints']['add_label'])) {
  300.                 throw new Exception($formItem['constraints']['add_label'] . ' debe ser texto');
  301.             }
  302.             $nativeFormItem['options']['attr']['add_label'] = $formItem['constraints']['add_label'];
  303.         }
  304.         // Remove label text (for type collections)
  305.         if (isset($formItem['constraints']['remove_label'])) {
  306.             if (!is_string($formItem['constraints']['remove_label'])) {
  307.                 throw new Exception($formItem['constraints']['remove_label'] . ' debe ser texto');
  308.             }
  309.             $nativeFormItem['options']['attr']['remove_label'] = $formItem['constraints']['remove_label'];
  310.         }
  311.         return $nativeFormItem;
  312.     }
  313.     /**
  314.      * @param array $nativeFormItem
  315.      * @param array $formItem
  316.      * @return array
  317.      * @throws Exception
  318.      */
  319.     private function configureNativeFormItemByType(array $nativeFormItem, array $formItem): array
  320.     {
  321.         switch ($formItem['type']) {
  322.             case self::TEXT_TYPE:
  323.                 $nativeFormItem['class'] = TextType::class;
  324.                 break;
  325.             case self::TEXTAREA_TYPE:
  326.                 $nativeFormItem['class'] = TextareaType::class;
  327.                 break;
  328.             case self::EMAIL_TYPE:
  329.                 $nativeFormItem['class'] = EmailType::class;
  330.                 $nativeFormItem['options']['constraints'][] = new Email(['mode' => 'strict']);
  331.                 break;
  332.             case self::NUMBER_TYPE:
  333.                 $nativeFormItem['options']['html5'] = true;
  334.                 $nativeFormItem['class'] = NumberType::class;
  335.                 break;
  336.             case self::SELECT_TYPE:
  337.                 $nativeFormItem['options']['choices'] = $this->buildFieldOptions($formItem['choices']);
  338.                 // Los catálogos de respuesta tienen que desplegarse en blanco: sin un
  339.                 // placeholder explícito, un <select> obligatorio renderiza preseleccionada
  340.                 // su primera opción y el usuario puede enviarla sin haberla elegido.
  341.                 $nativeFormItem['options']['placeholder'] = $formItem['attr']['placeholder'] ?? '';
  342.                 if (!empty($formItem['constraints']['required'])) {
  343.                     // `required` en Symfony es solo presentación, y el formulario se renderiza
  344.                     // con novalidate (la validación de navegador la lleva el JS), así que la
  345.                     // obligatoriedad de un select en blanco hay que comprobarla en servidor.
  346.                     $nativeFormItem['options']['constraints'][] = new NotBlank();
  347.                 }
  348.                 if (isset($formItem['attr']['value'])) {
  349.                     $nativeFormItem['options']['data'] = $formItem['attr']['value'];
  350.                 }
  351.                 $nativeFormItem['class'] = ChoiceType::class;
  352.                 break;
  353.             case self::CHECKBOX_TYPE:
  354.                 $nativeFormItem['options']['expanded'] = true;
  355.                 $nativeFormItem['options']['multiple'] = true;
  356.                 $nativeFormItem['options']['choices'] = $this->buildFieldOptions($formItem['choices']);
  357.                 $data = [];
  358.                 foreach ($formItem['choices'] as $choice) {
  359.                     if (isset($choice['checked']) && $choice['checked']) {
  360.                         $data[$choice['label']] = $choice['value'];
  361.                     }
  362.                 }
  363.                 if (!empty($data)) {
  364.                     $nativeFormItem['options']['data'] = $data;
  365.                 }
  366.                 $nativeFormItem['class'] = ChoiceType::class;
  367.                 break;
  368.             case self::RADIO_TYPE:
  369.                 $nativeFormItem['options']['expanded'] = true;
  370.                 $nativeFormItem['options']['multiple'] = false;
  371.                 $nativeFormItem['options']['choices'] = $this->buildFieldOptions($formItem['choices']);
  372.                 if (isset($formItem['attr']['value'])) {
  373.                     $nativeFormItem['options']['data'] = $formItem['attr']['value'];
  374.                 }
  375.                 $nativeFormItem['class'] = ChoiceType::class;
  376.                 break;
  377.             case self::DATE_TYPE:
  378.                 $nativeFormItem['options']['widget'] = 'single_text';
  379.                 $nativeFormItem['options']['html5'] = true;
  380.                 $nativeFormItem['class'] = DateType::class;
  381.                 break;
  382.             case self::DATETIME_TYPE:
  383.                 $nativeFormItem['options']['widget'] = 'single_text';
  384.                 $nativeFormItem['options']['html5'] = true;
  385.                 $nativeFormItem['class'] = DateTimeType::class;
  386.                 break;
  387.             case self::HIDDEN_TYPE:
  388.                 $nativeFormItem['class'] = HiddenType::class;
  389.                 if (isset($formItem['attr']['value'])) {
  390.                     $nativeFormItem['options']['data'] = $formItem['attr']['value'];
  391.                 }
  392.                 break;
  393.             case self::COLLECTION_TYPE:
  394.                 $items = [];
  395.                 foreach ($formItem['items'] as $item) {
  396.                     $items[] = $this->buildFormItem($item);
  397.                 }
  398.                 $nativeFormItem['class'] = CollectionType::class;
  399.                 $nativeFormItem['options']['entry_type'] = CollectionJsonType::class;
  400.                 $nativeFormItem['options']['entry_options'] = ['collection' => $items];
  401.                 $nativeFormItem['options']['allow_add'] = true;
  402.                 $nativeFormItem['options']['allow_delete'] = true;
  403.                 break;
  404.             case self::SUBMIT_TYPE:
  405.                 $nativeFormItem['class'] = SubmitType::class;
  406.                 break;
  407.             default:
  408.                 break;
  409.         }
  410.         return $nativeFormItem;
  411.     }
  412.     /**
  413.      * @param string $type
  414.      * @return bool
  415.      */
  416.     public static function formTypeIsSupported(string $type): bool
  417.     {
  418.         return in_array($type, self::SUPPORTED_TYPES);
  419.     }
  420.     /**
  421.      * @param array $choices
  422.      * @param bool $fieldIsRequired
  423.      * @return array
  424.      */
  425.     private function buildFieldOptions(array $choices, bool $fieldIsRequired = false): array
  426.     {
  427.         $nativeChoices = [];
  428.         foreach ($choices as $choice) {
  429.             $nativeChoices[$choice['label']] = $choice['value'];
  430.         }
  431.         return $nativeChoices;
  432.     }
  433.     /**
  434.      * @param string $fieldName
  435.      * @param $fieldValue
  436.      * @return string
  437.      * @throws Exception
  438.      */
  439.     public static function formFieldResponseToDb(string $fieldName, $fieldValue): string
  440.     {
  441.         $type = self::getTypeFromFieldName($fieldName);
  442.         if (!self::formTypeIsSupported($type)) {
  443.             throw new Exception(sprintf('El tipo %s no está soportado', $type));
  444.         }
  445.         switch ($type) {
  446.             case self::TEXT_TYPE:
  447.             case self::TEXTAREA_TYPE:
  448.             case self::EMAIL_TYPE:
  449.             case self::SELECT_TYPE:
  450.             case self::RADIO_TYPE:
  451.                 return $fieldValue;
  452.             case self::NUMBER_TYPE:
  453.                 return (str_replace(',', '.', $fieldValue));
  454.             case self::CHECKBOX_TYPE:
  455.                 return serialize($fieldValue);
  456.             case self::DATE_TYPE:
  457.                 return $fieldValue->format('Y-m-d');
  458.             case self::DATETIME_TYPE:
  459.                 return $fieldValue->format('Y-m-d H:i:s');
  460.             default:
  461.                 break;
  462.         }
  463.         return $fieldValue;
  464.     }
  465.     /**
  466.      * @param $field
  467.      * @param $type
  468.      * @return string
  469.      * @throws Exception
  470.      */
  471.     public static function formFieldDbItemToPrint($field, $type): string
  472.     {
  473.         if (!self::formTypeIsSupported($type)) {
  474.             throw new Exception(sprintf('El tipo %s no está soportado', $type));
  475.         }
  476.         switch ($type) {
  477.             case self::TEXT_TYPE:
  478.             case self::TEXTAREA_TYPE:
  479.             case self::EMAIL_TYPE:
  480.             case self::SELECT_TYPE:
  481.             case self::RADIO_TYPE:
  482.                 return $field;
  483.             case self::NUMBER_TYPE:
  484.                 return str_replace(".", ",", $field);
  485.             case self::CHECKBOX_TYPE:
  486.                 $unserialize = unserialize($field);
  487.                 if (is_string($unserialize) || $field === "") {
  488.                     return $unserialize;
  489.                 }
  490.                 return implode(self::CHECKBOX_VIEW_DELIMITER, $unserialize);
  491.             case self::DATE_TYPE:
  492.                 return (new DateTime($field))->format('d-m-Y');
  493.             case self::DATETIME_TYPE:
  494.                 return (new DateTime($field))->format('d-m-Y H:i');
  495.             default:
  496.                 break;
  497.         }
  498.         return $field;
  499.     }
  500.     /**
  501.      * @param string $fieldName
  502.      * @return string
  503.      */
  504.     public static function getNameFromFieldName(string $fieldName): string
  505.     {
  506.         return self::explodeFieldName($fieldName, 0);
  507.     }
  508.     /**
  509.      * @param string $fieldName
  510.      * @return string
  511.      */
  512.     public static function getTypeFromFieldName(string $fieldName): string
  513.     {
  514.         return self::explodeFieldName($fieldName, 1);
  515.     }
  516.     /**
  517.      * @return array
  518.      * @throws Exception
  519.      */
  520.     public function getFormDefinitionFields(): array
  521.     {
  522.         $result = [];
  523.         foreach ($this->getFormDefinition() as $item) {
  524.             if (!$this->formItemIsGroup($item)) {
  525.                 // Is field
  526.                 $result[] = $item;
  527.                 continue;
  528.             }
  529.             foreach ($item['fields'] as $field) {
  530.                 $result[] = $field;
  531.             }
  532.         }
  533.         return $result;
  534.     }
  535.     /**
  536.      * @param array $formData
  537.      * @param array $formDefinitionFields
  538.      * @return array
  539.      */
  540.     public function getFormDataInFormDefinitionFields(array $formData, array $formDefinitionFields): array
  541.     {
  542.         foreach ($formData as $key => $value) {
  543.             foreach ($formDefinitionFields as $field) {
  544.                 if ($field['name'] !== $key) {
  545.                     continue;
  546.                 }
  547.                 // TODO: hardcoded
  548.                 $formData = $this->processPayment($formData, $field, $key, $value);
  549.                 // Check not binding fields
  550.                 if (isset($field['options']['attr']['bind']) && !$field['options']['attr']['bind']) {
  551.                     unset($formData[$key]);
  552.                 }
  553.             }
  554.         }
  555.         return $formData;
  556.     }
  557.     /**
  558.      * @param string $incompatibleValueString
  559.      * @return mixed|string
  560.      */
  561.     public function getIncompatibleGroup(string $incompatibleValueString)
  562.     {
  563.         return $this->getExplodedIncompatibleField($incompatibleValueString)[0];
  564.     }
  565.     /**
  566.      * @param string $incompatibleValueString
  567.      * @return false|string[]
  568.      */
  569.     public function getExplodedIncompatibleField(string $incompatibleValueString)
  570.     {
  571.         return explode(self::INCOMPATIBLE_FIELD_DELIMITER, $incompatibleValueString);
  572.     }
  573.     /**
  574.      * @param string $value
  575.      * @return string
  576.      */
  577.     public static function unFormattedHtml(string $value): string
  578.     {
  579.         return $value != strip_tags($value) ? strip_tags($value) : $value;
  580.     }
  581.     /**
  582.      * @param array $formItem
  583.      * @return bool
  584.      */
  585.     private function formItemIsGroup(array $formItem): bool
  586.     {
  587.         return isset($formItem['fields']);
  588.     }
  589.     /**
  590.      * @param array $nativeFormItem
  591.      * @param array $formItem
  592.      * @return array
  593.      * @throws Exception
  594.      */
  595.     private function buildConstraints(array $nativeFormItem, array $formItem): array
  596.     {
  597.         if (
  598.             !isset($formItem['constraints']['validators']) &&
  599.             !isset($formItem['constraints']['unique_by']) &&
  600.             !isset($formItem['constraints']['incompatible_with'])
  601.         ) {
  602.             return $nativeFormItem;
  603.         }
  604.         if (isset($formItem['constraints']['unique_by'])) {
  605.             $nativeFormItem = $this->buildConstraintUniqueBy($nativeFormItem, $formItem);
  606.         }
  607.         if (isset($formItem['constraints']['incompatible_with'])) {
  608.             $nativeFormItem = $this->buildIncompatibleField($nativeFormItem, $formItem);
  609.         }
  610.         if (isset($formItem['constraints']['validators'])) {
  611.             $nativeFormItem = $this->buildConstraintValidators($nativeFormItem, $formItem);
  612.         }
  613.         return $nativeFormItem;
  614.     }
  615.     /**
  616.      * @param array $nativeFormItem
  617.      * @param array $formItem
  618.      * @return array
  619.      */
  620.     private function buildConstraintUniqueBy(array $nativeFormItem, array $formItem): array
  621.     {
  622.         $entity = null;
  623.         $campaign = $this->getForm()->getCampaign();
  624.         $company = $campaign->getCompany();
  625.         switch ($formItem['constraints']['unique_by']) {
  626.             case $this->getForm()->getCampaign()->getClass():
  627.                 $entity = $campaign;
  628.                 break;
  629.             case $this->getForm()->getCampaign()->getCompany()->getClass():
  630.                 $entity = $company;
  631.         }
  632.         $nativeFormItem['options']['constraints'][] = new UniqueLeadDataBy(['entity' => $entity]);
  633.         return $nativeFormItem;
  634.     }
  635.     /**
  636.      * @param array $nativeFormItem
  637.      * @param array $formItem
  638.      * @return array
  639.      */
  640.     private function buildIncompatibleField(array $nativeFormItem, array $formItem): array
  641.     {
  642.         $indexes = ['group', 'name', 'type'];
  643.         foreach ($formItem['constraints']['incompatible_with'] as $key => $value) {
  644.             $value = '';
  645.             foreach ($indexes as $index) {
  646.                 $value .= $formItem['constraints']['incompatible_with'][$key][$index] . self::INCOMPATIBLE_FIELD_DELIMITER;
  647.             }
  648.             $nativeFormItem['options']['attr']["incompatible-with-$key"] = substr(
  649.                 $value,
  650.                 0,
  651.                 -strlen(self::INCOMPATIBLE_FIELD_DELIMITER)
  652.             );
  653.         }
  654.         return $nativeFormItem;
  655.     }
  656.     /**
  657.      * @param array $nativeFormItem
  658.      * @param array $formItem
  659.      * @return array
  660.      * @throws Exception
  661.      */
  662.     private function buildConstraintValidators(array $nativeFormItem, array $formItem): array
  663.     {
  664.         foreach ($formItem['constraints']['validators'] as $validator => $params) {
  665.             $validatorNamespaces = [
  666.                 'App\Validator',
  667.                 '\Symfony\Component\Validator\Constraints'
  668.             ];
  669.             $className = null;
  670.             foreach ($validatorNamespaces as $validatorNamespace) {
  671.                 $className = $validatorNamespace . '\\' . $validator;
  672.                 if ($this->validator->hasMetadataFor($className)) {
  673.                     break;
  674.                 }
  675.             }
  676.             if (!$className) {
  677.                 throw new Exception();
  678.             }
  679.             if (!empty($params)) {
  680.                 $nativeFormItem['options']['constraints'][] = new $className($params);
  681.             } else {
  682.                 $nativeFormItem['options']['constraints'][] = new $className();
  683.             }
  684.         }
  685.         return $nativeFormItem;
  686.     }
  687.     /**
  688.      * @param string $fieldName
  689.      * @param int $index
  690.      * @return string
  691.      */
  692.     private static function explodeFieldName(string $fieldName, int $index): string
  693.     {
  694.         return explode(self::FIELDNAME_DELIMITER, $fieldName)[$index];
  695.     }
  696.     /**
  697.      * @param string $name
  698.      * @param string $type
  699.      * @return string
  700.      */
  701.     public static function buildFieldName(string $name, string $type): string
  702.     {
  703.         return $name . self::FIELDNAME_DELIMITER . $type;
  704.     }
  705.     /**
  706.      * Clave de agrupación de FormLeadData: name_type, y name_type#N para el elemento N de un
  707.      * apartado repetible.
  708.      *
  709.      * Los elementos de una colección comparten field y type, así que agrupar solo por
  710.      * name_type los interpretaba como versiones sucesivas del mismo campo y se quedaba
  711.      * únicamente con el último.
  712.      *
  713.      * @param string $name
  714.      * @param string $type
  715.      * @param int|null $collectionIndex
  716.      * @return string
  717.      */
  718.     public static function buildLeadDataKey(string $name, string $type, ?int $collectionIndex = null): string
  719.     {
  720.         $fieldName = self::buildFieldName($name, $type);
  721.         return null === $collectionIndex
  722.             ? $fieldName
  723.             : $fieldName . self::COLLECTION_INDEX_DELIMITER . $collectionIndex;
  724.     }
  725.     /**
  726.      * name_type de una clave de buildLeadDataKey(), sin el índice de colección.
  727.      *
  728.      * @param string $key
  729.      * @return string
  730.      */
  731.     public static function getFieldNameFromLeadDataKey(string $key): string
  732.     {
  733.         return explode(self::COLLECTION_INDEX_DELIMITER, $key)[0];
  734.     }
  735.     /**
  736.      * Índices presentes por subcampo de colección, a partir del valor vigente de cada clave.
  737.      *
  738.      * Un elemento vaciado al editar (se reduce el número de elementos) deja como vigente un
  739.      * valor '' y no cuenta como presente.
  740.      *
  741.      * @param iterable<string, FormLeadData> $latestByKey Vigente por buildLeadDataKey()
  742.      * @return array<string, int[]> name_type => índices
  743.      */
  744.     public static function collectCollectionIndexes(iterable $latestByKey): array
  745.     {
  746.         $result = [];
  747.         foreach ($latestByKey as $data) {
  748.             if (null === $data->getCollectionIndex() || '' === (string) $data->getValue()) {
  749.                 continue;
  750.             }
  751.             $result[self::buildFieldName($data->getField(), $data->getType())][] = $data->getCollectionIndex();
  752.         }
  753.         return $result;
  754.     }
  755.     /**
  756.      * Sustituye en el esquema cada apartado repetible por una entrada por elemento y subcampo.
  757.      *
  758.      * buildFieldsSchema() aporta una sola entrada por subcampo de colección, pero cuántos
  759.      * elementos hay lo decide cada registro. El orden resultante es por elemento ("Nombre 1",
  760.      * "Edad 1", "Nombre 2"...), igual que se guardan, con el ordinal en la etiqueta como hace
  761.      * el alta. Las entradas expandidas se indexan con buildLeadDataKey() y llevan
  762.      * 'collectionIndex'; el resto del esquema pasa tal cual.
  763.      *
  764.      * @param array<string, array> $schema
  765.      * @param array<string, int[]> $indexesByField name_type => índices presentes
  766.      * @param int $cap
  767.      * @return array<string, array>
  768.      */
  769.     public static function expandCollectionSchema(array $schema, array $indexesByField, int $cap = self::MAX_COLLECTION_ITEMS): array
  770.     {
  771.         // Subcampos de cada colección, en el orden del esquema
  772.         $collections = [];
  773.         foreach ($schema as $fieldName => $meta) {
  774.             if (!empty($meta['collection'])) {
  775.                 $collections[$meta['collection']][$fieldName] = $meta;
  776.             }
  777.         }
  778.         $result = [];
  779.         $expanded = [];
  780.         foreach ($schema as $fieldName => $meta) {
  781.             $collection = $meta['collection'] ?? null;
  782.             if (!$collection) {
  783.                 $result[$fieldName] = $meta;
  784.                 continue;
  785.             }
  786.             if (isset($expanded[$collection])) {
  787.                 continue;
  788.             }
  789.             $expanded[$collection] = true;
  790.             $indexes = [];
  791.             foreach (array_keys($collections[$collection]) as $subFieldName) {
  792.                 foreach ($indexesByField[$subFieldName] ?? [] as $index) {
  793.                     $indexes[$index] = $index;
  794.                 }
  795.             }
  796.             sort($indexes);
  797.             foreach (array_slice($indexes, 0, $cap) as $index) {
  798.                 foreach ($collections[$collection] as $subFieldName => $subMeta) {
  799.                     $key = self::buildLeadDataKey($subMeta['field'], $subMeta['type'], $index);
  800.                     $result[$key] = array_merge($subMeta, [
  801.                         'label' => $subMeta['label'] . ' ' . $index,
  802.                         'collectionIndex' => $index,
  803.                     ]);
  804.                 }
  805.             }
  806.         }
  807.         return $result;
  808.     }
  809.     /**
  810.      * @param string $name
  811.      * @param string $type
  812.      * @param string $label
  813.      * @param string $groupLabel
  814.      * @return string
  815.      */
  816.     public static function buildFieldIndexLabel(string $name, string $type, string $label, string $groupLabel): string
  817.     {
  818.         return
  819.             $name . self::FIELDNAME_DELIMITER .
  820.             $type . self::FIELDNAME_DELIMITER .
  821.             self::unFormattedHtml($label) . self::FIELDNAME_DELIMITER .
  822.             self::unFormattedHtml($groupLabel);
  823.     }
  824.     /**
  825.      * @return array
  826.      * @throws Exception
  827.      */
  828.     public function buildCsvHeader(): array
  829.     {
  830.         $result = [];
  831.         foreach ($this->buildFieldsSchema() as $index => $item) {
  832.             $result[$index]['label'] = $item['label'];
  833.             $result[$index]['group_label'] = $item['groupLabel'];
  834.         }
  835.         return $result;
  836.     }
  837.     /**
  838.      * Campos persistibles de la definición vigente, en el orden en que aparecen en el JSON.
  839.      *
  840.      * Es la fuente única de verdad para saber qué campos tiene el formulario ahora mismo,
  841.      * frente a las etiquetas que quedaron guardadas en FormLeadData al enviarlo.
  842.      *
  843.      * La clave es buildFieldName() ("empresa_text"), que es la clave natural de FormLeadData
  844.      * y la única estable: hay grupos con varios campos que comparten etiqueta (cinco "Otros"
  845.      * en PRINCIPIO 6), así que indexar por label los colapsaría en uno.
  846.      *
  847.      * Los subcampos de un apartado repetible aportan una sola entrada, marcada con el nombre
  848.      * de su colección en 'collection'; expandCollectionSchema() la desdobla por elemento.
  849.      *
  850.      * @return array<string, array{field: string, type: string, label: string, groupLabel: string, groupName: string, collection: string|null}>
  851.      * @throws Exception
  852.      */
  853.     public function buildFieldsSchema(): array
  854.     {
  855.         $result = [];
  856.         foreach ($this->getFormDefinition() as $item) {
  857.             if (!$this->formItemIsGroup($item)) {
  858.                 // Campo suelto, fuera de grupo
  859.                 $this->appendFieldToSchema($result, $item, '', '');
  860.                 continue;
  861.             }
  862.             $groupLabel = self::unFormattedHtml((string) ($item['options']['label'] ?? ''));
  863.             foreach ($item['fields'] as $field) {
  864.                 $this->appendFieldToSchema($result, $field, $groupLabel, (string) ($item['name'] ?? ''));
  865.             }
  866.         }
  867.         return $result;
  868.     }
  869.     /**
  870.      * @param array $result
  871.      * @param array $field
  872.      * @param string $groupLabel
  873.      * @param string $groupName
  874.      * @param string|null $collection Colección a la que pertenece el campo, si es un subcampo
  875.      * @return void
  876.      */
  877.     private function appendFieldToSchema(
  878.         array &$result,
  879.         array $field,
  880.         string $groupLabel,
  881.         string $groupName,
  882.         ?string $collection = null
  883.     ): void {
  884.         $fieldName = $field['name'];
  885.         $type = self::getTypeFromFieldName($fieldName);
  886.         // Las colecciones aportan una entrada por cada campo de su item
  887.         if (self::COLLECTION_TYPE === $type) {
  888.             foreach ($field['options']['entry_options']['collection'] ?? [] as $entry) {
  889.                 $this->appendFieldToSchema($result, $entry, $groupLabel, $groupName, $fieldName);
  890.             }
  891.             return;
  892.         }
  893.         // Fuera lo que no llega a BD: submit, tipos no persistibles y campos con bind = false
  894.         if (!isset(self::VALUABLE_TYPES[$type])) {
  895.             return;
  896.         }
  897.         if (isset($field['options']['attr']['bind']) && !$field['options']['attr']['bind']) {
  898.             return;
  899.         }
  900.         $result[$fieldName] = [
  901.             'field' => self::getNameFromFieldName($fieldName),
  902.             'type' => $type,
  903.             'label' => $this->resolveSchemaLabel($field, $groupLabel),
  904.             'groupLabel' => $groupLabel,
  905.             'groupName' => $groupName,
  906.             'collection' => $collection,
  907.         ];
  908.     }
  909.     /**
  910.      * common.label puede venir vacío o a false en el JSON; se cae al placeholder y, en último
  911.      * término, al propio nombre del campo, para que nunca quede una columna sin cabecera.
  912.      *
  913.      * @param array $field
  914.      * @return string
  915.      */
  916.     private function resolveSchemaLabel(array $field, string $groupLabel = ''): string
  917.     {
  918.         // Un campo con "label": false (el título ya lo da su grupo) se etiqueta con el del grupo
  919.         $label = ($field['options']['label'] ?? null)
  920.             ?: ($field['options']['attr']['placeholder'] ?? null)
  921.             ?: $groupLabel;
  922.         return self::unFormattedHtml((string) ($label ?: self::getNameFromFieldName($field['name'])));
  923.     }
  924.     /**
  925.      * @return string
  926.      */
  927.     public function getBasePath(): string
  928.     {
  929.         return $this->params->get('kernel.project_dir') . "/" . self::FOLDER_NAME;
  930.     }
  931.     /**
  932.      * @param array $nativeFormItem
  933.      * @param array $formItem
  934.      * @return array
  935.      */
  936.     private function getConditionedFields(array $nativeFormItem, array $formItem): array
  937.     {
  938.         if (!in_array($formItem['type'], self::SELECTABLE_TYPES)) {
  939.             return $nativeFormItem;
  940.         }
  941.         if (!isset($formItem['choices'])) {
  942.             return $nativeFormItem;
  943.         }
  944.         $conditionedFields = [];
  945.         foreach ($formItem['choices'] as $keyChoice => $choice) {
  946.             if (!isset($choice['conditional_on']) || !$choice['conditional_on']) {
  947.                 continue;
  948.             }
  949.             $conditionedFields[$keyChoice]['parent_value'] = $choice['value'];
  950.             foreach ($choice['conditional_on'] as $key => $item) {
  951.                 $conditionedFields[$keyChoice]['conditioned_fields'][$key] = $item;
  952.                 $conditionedFields[$keyChoice]['conditioned_fields'][$key]['field_name'] = self::buildNativeFieldName(
  953.                     isset($this->findFieldGroup($formItem)['name']) ? $this->findFieldGroup($formItem)['name'] : '',
  954.                     $item['field']['name'],
  955.                     $item['field']['type']
  956.                 );
  957.                 unset($conditionedFields[$keyChoice]['conditioned_fields'][$key]['field']);
  958.             }
  959.         }
  960.         if (!empty($conditionedFields)) {
  961.             $nativeFormItem['options']['attr']['data-conditioned-fields'] = json_encode($conditionedFields, true);
  962.         }
  963.         return $nativeFormItem;
  964.     }
  965.     /**
  966.      * @param array $formItem
  967.      * @return array|mixed|void
  968.      */
  969.     private function findFieldGroup(array $formItem)
  970.     {
  971.         foreach ($this->formDefinition as $group) {
  972.             if (!$this->formItemIsGroup($group)) {
  973.                 continue;
  974.             }
  975.             foreach ($group['fields'] as $field) {
  976.                 if ($field['type'] === $formItem['type'] && $field['common']['name'] === $formItem['common']['name']) {
  977.                     return $group;
  978.                 }
  979.             }
  980.         }
  981.         return [];
  982.     }
  983.     /**
  984.      * @param string $groupName
  985.      * @param string $name
  986.      * @param string $type
  987.      * @return string
  988.      */
  989.     private function buildNativeFieldName(string $groupName, string $name, string $type): string
  990.     {
  991.         return 'json_' . $groupName . self::FIELDNAME_DELIMITER . self::buildFieldName($name, $type);
  992.     }
  993.     /**
  994.      * @return void
  995.      */
  996.     private function addFieldToDefinition()
  997.     {
  998.         if (!$this->queryParams) {
  999.             return;
  1000.         }
  1001.         foreach ($this->queryParams as $key => $param) {
  1002.             foreach ($this->formDefinition as $keyGroup => $group) {
  1003.                 if (!$this->formItemIsGroup($group)) {
  1004.                     continue;
  1005.                 }
  1006.                 foreach ($group['fields'] as $keyField => $field) {
  1007.                     if ($key !== $field['common']['name']) {
  1008.                         continue;
  1009.                     }
  1010.                     $value = null;
  1011.                     $readOnly = false;
  1012.                     switch ($field['type']) {
  1013.                         case self::EMAIL_TYPE:
  1014.                         case self::TEXT_TYPE:
  1015.                         case self::NUMBER_TYPE:
  1016.                             $value = $param;
  1017.                             $readOnly = true;
  1018.                         break;
  1019.                         case self::HIDDEN_TYPE:
  1020.                             $value = $param;
  1021.                             break;
  1022.                         case self::DATE_TYPE:
  1023.                             if (DateTimeService::isDate($param)) {
  1024.                                 $value = $param;
  1025.                                 $readOnly = true;
  1026.                             }
  1027.                             break;
  1028.                         case self::DATETIME_TYPE:
  1029.                             if (DateTimeService::isDateTime($param)) {
  1030.                                 $value = $param;
  1031.                                 $readOnly = true;
  1032.                             }
  1033.                             break;
  1034.                         case self::RADIO_TYPE:
  1035.                         case self::SELECT_TYPE:
  1036.                             if (!isset($field['choices'])) {
  1037.                                 break;
  1038.                             }
  1039.                             foreach ($field['choices'] as $choice) {
  1040.                                 if ($choice['value'] !== $param) {
  1041.                                     continue;
  1042.                                 }
  1043.                                 $value = $param;
  1044.                                 $readOnly = true;
  1045.                                 break;
  1046.                             }
  1047.                             break;
  1048.                         case self::CHECKBOX_TYPE:
  1049.                             $value = $param;
  1050.                             $readOnly = true;
  1051.                             $this->formDefinition[$keyGroup]['fields'][$keyField]['choices'][0]['label'] = $param;
  1052.                             break;
  1053.                     }
  1054.                     if (!$value) {
  1055.                         continue;
  1056.                     }
  1057.                     $this->formDefinition[$keyGroup]['fields'][$keyField]['attr']['value'] = $value;
  1058.                     if ($readOnly) {
  1059.                         $this->formDefinition[$keyGroup]['fields'][$keyField]['attr']['readonly'] = true;
  1060.                     }
  1061.                 }
  1062.             }
  1063.         }
  1064.     }
  1065.     /**
  1066.      * @TODO: hardcoded
  1067.      * @param array $formData
  1068.      * @param array $field
  1069.      * @param string $key
  1070.      * @param $value
  1071.      * @return array
  1072.      */
  1073.     private function processPayment(array $formData, array $field, string $key, $value): array
  1074.     {
  1075.         $paymentExentionKey = 'aportacion-economica-exenciones_checkbox';
  1076.         if ($paymentExentionKey === $field['name'] && !$value) {
  1077.             $formData[$key] = ['Ninguno'];
  1078.             return $formData;
  1079.         }
  1080.         if ($paymentExentionKey === $field['name'] && $value) {
  1081.             $formData['preferencia-pago-aportacion-economica_radio'] = '-';
  1082.             $formData['preferencia-domiciliar-pago-aportacion-economica_radio'] = '-';
  1083.         }
  1084.         return $formData;
  1085.     }
  1086. }