vendor/dachcom-digital/formbuilder/src/FormBuilderBundle/EventSubscriber/FormBuilderSubscriber.php line 96

Open in your IDE?
  1. <?php
  2. namespace FormBuilderBundle\EventSubscriber;
  3. use FormBuilderBundle\Registry\DataInjectionRegistry;
  4. use FormBuilderBundle\Validator\Constraints\DynamicMultiFileNotBlank;
  5. use FormBuilderBundle\Model\FieldDefinitionInterface;
  6. use FormBuilderBundle\Model\FormFieldContainerDefinitionInterface;
  7. use FormBuilderBundle\Model\FormFieldDefinitionInterface;
  8. use FormBuilderBundle\Model\FormFieldDynamicDefinitionInterface;
  9. use FormBuilderBundle\Configuration\Configuration;
  10. use FormBuilderBundle\Form\Data\FormDataInterface;
  11. use FormBuilderBundle\Event\Form\PostSetDataEvent;
  12. use FormBuilderBundle\Event\Form\PreSetDataEvent;
  13. use FormBuilderBundle\Event\Form\PreSubmitEvent;
  14. use FormBuilderBundle\FormBuilderEvents;
  15. use FormBuilderBundle\Validation\ConditionalLogic\Dispatcher\Dispatcher;
  16. use FormBuilderBundle\Validation\ConditionalLogic\Dispatcher\Module\Data\DataInterface;
  17. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  18. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  19. use Symfony\Component\Form\FormEvent;
  20. use Symfony\Component\Form\FormEvents;
  21. use Symfony\Component\Form\FormInterface;
  22. use Symfony\Component\Form\FormRegistryInterface;
  23. use Symfony\Component\Validator\Constraints\NotBlank;
  24. class FormBuilderSubscriber implements EventSubscriberInterface
  25. {
  26.     private array $availableConstraints;
  27.     private array $availableFormTypes;
  28.     public function __construct(
  29.         protected Configuration $configuration,
  30.         protected EventDispatcherInterface $eventDispatcher,
  31.         protected Dispatcher $dispatcher,
  32.         protected FormRegistryInterface $formRegistry,
  33.         protected DataInjectionRegistry $dataInjectionRegistry
  34.     ) {
  35.         $this->availableConstraints $this->configuration->getAvailableConstraints();
  36.         $this->availableFormTypes $this->configuration->getConfig('types');
  37.     }
  38.     public static function getSubscribedEvents(): array
  39.     {
  40.         return [
  41.             FormEvents::PRE_SET_DATA  => ['onPreSetData'],
  42.             FormEvents::POST_SET_DATA => ['onPostSetData'],
  43.             FormEvents::PRE_SUBMIT    => ['onPreSubmit']
  44.         ];
  45.     }
  46.     /**
  47.      * @throws \Exception
  48.      */
  49.     public function getFormOptions(FormEvent $event): ?array
  50.     {
  51.         $form $event->getForm();
  52.         if (!$form->has('formRuntimeData')) {
  53.             throw new \Exception('No runtime options in form found.');
  54.         }
  55.         $data $form->get('formRuntimeData')->getData();
  56.         // remove legacy email config node.
  57.         if (isset($data['email'])) {
  58.             unset($data['email']);
  59.         }
  60.         return $data;
  61.     }
  62.     /**
  63.      * @throws \Exception
  64.      */
  65.     public function onPreSetData(FormEvent $event): void
  66.     {
  67.         $preSetDataEvent = new PreSetDataEvent($event$this->getFormOptions($event));
  68.         $this->eventDispatcher->dispatch($preSetDataEventFormBuilderEvents::FORM_PRE_SET_DATA);
  69.     }
  70.     /**
  71.      * @throws \Exception
  72.      */
  73.     public function onPostSetData(FormEvent $event): void
  74.     {
  75.         $postSetDataEvent = new PostSetDataEvent($event$this->getFormOptions($event));
  76.         $this->eventDispatcher->dispatch($postSetDataEventFormBuilderEvents::FORM_POST_SET_DATA);
  77.         $this->populateForm($event->getForm(), $event->getData());
  78.     }
  79.     /**
  80.      * @throws \Exception
  81.      */
  82.     public function onPreSubmit(FormEvent $event): void
  83.     {
  84.         $preSubmitEvent = new PreSubmitEvent($event$this->getFormOptions($event));
  85.         $this->eventDispatcher->dispatch($preSubmitEventFormBuilderEvents::FORM_PRE_SUBMIT);
  86.         $this->populateForm($event->getForm(), $event->getForm()->getData(), $event->getData());
  87.     }
  88.     /**
  89.      * @throws \Exception
  90.      */
  91.     private function populateForm(FormInterface $formFormDataInterface $formData, array $data = []): void
  92.     {
  93.         $orderedFields $formData->getFormDefinition()->getFields();
  94.         usort($orderedFields, function (FieldDefinitionInterface $aFieldDefinitionInterface $b) {
  95.             return ($a->getOrder() < $b->getOrder()) ? -1;
  96.         });
  97.         $data $this->preFillData($orderedFields$data);
  98.         $formRuntimeOptions = !$form->has('formRuntimeData') ? [] : $form->get('formRuntimeData')->getData();
  99.         $conditionalLogicBaseOptions = [
  100.             'formRuntimeOptions' => $formRuntimeOptions,
  101.             'conditionalLogic'   => $formData->getFormDefinition()->getConditionalLogic()
  102.         ];
  103.         foreach ($orderedFields as $field) {
  104.             if ($field instanceof FormFieldDynamicDefinitionInterface) {
  105.                 $formTypeData $this->addDynamicField($field);
  106.                 $form->add($formTypeData['name'], $formTypeData['type'], $formTypeData['options']);
  107.             } elseif ($field instanceof FormFieldContainerDefinitionInterface) {
  108.                 $conditionalLogicOptions array_merge(['formData' => $data'field' => null], $conditionalLogicBaseOptions);
  109.                 $formTypeData $this->addFormBuilderContainerField($field$conditionalLogicOptions);
  110.                 $form->add($formTypeData['name'], $formTypeData['type'], $formTypeData['options']);
  111.             } else {
  112.                 $conditionalLogicOptions array_merge(['formData' => $data'field' => $field], $conditionalLogicBaseOptions);
  113.                 $formTypeData $this->addFormBuilderField($field$conditionalLogicOptions);
  114.                 $form->add($formTypeData['name'], $formTypeData['type'], $formTypeData['options']);
  115.             }
  116.         }
  117.     }
  118.     /**
  119.      * @throws \Exception
  120.      */
  121.     private function addFormBuilderContainerField(FormFieldContainerDefinitionInterface $fieldContainer, array $conditionalLogicOptions): array
  122.     {
  123.         $fields = [];
  124.         foreach ($fieldContainer->getFields() as $subField) {
  125.             $fields[] = $this->addFormBuilderField($subFieldarray_merge($conditionalLogicOptions, ['field' => $subField]));
  126.         }
  127.         $typeClass $this->configuration->getContainerFieldClass($fieldContainer->getSubType());
  128.         $configuration $fieldContainer->getConfiguration();
  129.         $containerAttributes $configuration['attr'] ?? [];
  130.         $containerClasses = ['formbuilder-container formbuilder-container-' strtolower($fieldContainer->getSubType())];
  131.         if (isset($containerAttributes['class']) && is_string($containerAttributes['class'])) {
  132.             $containerClasses[] = $containerAttributes['class'];
  133.         }
  134.         // merge core and attributes class definition
  135.         $containerAttributes['class'] = implode(' '$containerClasses);
  136.         // options enrichment: conditional logic class mapping
  137.         $conditionalContainerClassData $this->dispatchConditionalLogicModule('form_type_classes'array_merge($conditionalLogicOptions, ['field' => $fieldContainer]));
  138.         if ($conditionalContainerClassData->hasData()) {
  139.             $attrDataTemplate = isset($containerAttributes['data-template']) ? [$containerAttributes['data-template']] : [];
  140.             $attrDataTemplate array_merge($attrDataTemplate$conditionalContainerClassData->getData());
  141.             $containerAttributes['data-template'] = implode(' '$attrDataTemplate);
  142.         }
  143.         return [
  144.             'name'    => $fieldContainer->getName(),
  145.             'type'    => $typeClass,
  146.             'options' => [
  147.                 'attr'                      => $containerAttributes,
  148.                 'formbuilder_configuration' => $configuration,
  149.                 'entry_options'             => [
  150.                     'fields'         => $fields,
  151.                     'container_type' => $fieldContainer->getSubType()
  152.                 ]
  153.             ]
  154.         ];
  155.     }
  156.     /**
  157.      * @throws \Exception
  158.      */
  159.     private function addFormBuilderField(FormFieldDefinitionInterface $field, array $conditionalLogicOptions): array
  160.     {
  161.         $options $field->getOptions();
  162.         $optional $field->getOptional();
  163.         $object $this->formRegistry->getType($this->availableFormTypes[$field->getType()]['class'])->getOptionsResolver();
  164.         $availableOptions $object->getDefinedOptions();
  165.         $constraints = [];
  166.         $constraintNames = [];
  167.         $templateClasses = [];
  168.         // options enrichment: tweak preferred choice options
  169.         if (in_array($field->getType(), $this->getChoiceFieldTypes(), true)) {
  170.             if (isset($options['multiple']) && $options['multiple'] === false && array_key_exists('data'$options) && is_array($options['data'])) {
  171.                 $options['data'] = $options['data'][0];
  172.             }
  173.         }
  174.         if (array_key_exists('dataInjection'$options) && !empty($options['dataInjection'])) {
  175.             $dataInjection json_decode($options['dataInjection'], true);
  176.             unset($options['dataInjection']);
  177.             if ($this->dataInjectionRegistry->has($dataInjection['injector'])) {
  178.                 $options['data'] = $this->dataInjectionRegistry->get($dataInjection['injector'])->parseData($dataInjection['config']);
  179.             }
  180.         }
  181.         // options enrichment: add constraints
  182.         if (in_array('constraints'$availableOptions)) {
  183.             $conditionalConstraintData $this->dispatchConditionalLogicModule('constraints'$conditionalLogicOptions);
  184.             // add field constraints to data attribute since we need them for the frontend cl applier.
  185.             foreach ($field->getConstraints() as $constraint) {
  186.                 $constraintNames[] = $constraint['type'];
  187.             }
  188.             if ($conditionalConstraintData->hasData()) {
  189.                 $constraints $conditionalConstraintData->getData();
  190.                 $options['constraints'] = $constraints;
  191.             }
  192.         }
  193.         $options['attr']['data-initial-constraints'] = join(','$constraintNames);
  194.         // options enrichment: check required state
  195.         if (in_array('required'$availableOptions)) {
  196.             $options['required'] = count(
  197.                     array_filter($constraints, function ($constraint) {
  198.                         return $constraint instanceof NotBlank || $constraint instanceof DynamicMultiFileNotBlank;
  199.                     })
  200.                 ) === 1;
  201.         }
  202.         // options enrichment: check for custom radio / checkbox layout
  203.         if ($this->configuration->getConfigFlag('use_custom_radio_checkbox') === true) {
  204.             if (in_array('label_attr'$availableOptions)) {
  205.                 if ($field->getType() === 'checkbox') {
  206.                     $options['label_attr'] = ['class' => 'checkbox-custom'];
  207.                 } elseif (in_array($field->getType(), $this->getChoiceFieldTypes())) {
  208.                     if (isset($options['expanded']) && $options['expanded'] === true) {
  209.                         $options['label_attr'] = ['class' => $options['multiple'] === true 'checkbox-custom' 'radio-custom'];
  210.                     }
  211.                 }
  212.             }
  213.         }
  214.         // options enrichment: set template
  215.         if (isset($optional['template'])) {
  216.             $templateClasses[] = $optional['template'];
  217.         }
  218.         // options enrichment: conditional logic class mapping
  219.         $conditionalClassData $this->dispatchConditionalLogicModule('form_type_classes'$conditionalLogicOptions);
  220.         if ($conditionalClassData->hasData()) {
  221.             $templateClasses array_merge($templateClasses$conditionalClassData->getData());
  222.         }
  223.         if (!empty($templateClasses)) {
  224.             $options['attr']['data-template'] = implode(' '$templateClasses);
  225.         }
  226.         return [
  227.             'name'    => $field->getName(),
  228.             'type'    => $this->availableFormTypes[$field->getType()]['class'],
  229.             'options' => $options
  230.         ];
  231.     }
  232.     /**
  233.      * @throws \Exception
  234.      */
  235.     private function dispatchConditionalLogicModule(string $dispatcherModule, array $options): DataInterface
  236.     {
  237.         $moduleOptions = [];
  238.         if ($dispatcherModule === 'constraints') {
  239.             $moduleOptions = [
  240.                 'availableConstraints' => $this->availableConstraints
  241.             ];
  242.         }
  243.         return $this->dispatcher->runFieldDispatcher($dispatcherModule$options$moduleOptions);
  244.     }
  245.     private function addDynamicField(FormFieldDynamicDefinitionInterface $field): array
  246.     {
  247.         $options $field->getOptions();
  248.         $optional $field->getOptional();
  249.         //set optional template
  250.         if (isset($optional['template'])) {
  251.             $options['attr']['data-template'] = $optional['template'];
  252.         }
  253.         return [
  254.             'name'    => $field->getName(),
  255.             'type'    => $field->getType(),
  256.             'options' => $options
  257.         ];
  258.     }
  259.     private function getChoiceFieldTypes(): array
  260.     {
  261.         return ['choice''dynamic_choice''country'];
  262.     }
  263.     private function preFillData(array $fields, array &$data): array
  264.     {
  265.         /** @var FormFieldDefinitionInterface $field */
  266.         foreach ($fields as $field) {
  267.             if (!empty($data[$field->getName()])) {
  268.                 continue;
  269.             }
  270.             if ($field instanceof FormFieldContainerDefinitionInterface) {
  271.                 if (!isset($data[$field->getName()])) {
  272.                     $data[$field->getName()] = [];
  273.                 }
  274.                 $this->preFillData($field->getFields(), $data[$field->getName()]);
  275.                 continue;
  276.             }
  277.             $fieldOptions $field->getOptions();
  278.             if (isset($fieldOptions['data'])) {
  279.                 $data[$field->getName()] = $fieldOptions['data'];
  280.             }
  281.         }
  282.         return $data;
  283.     }
  284. }