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

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