Skip to content

Commit

Permalink
allow option configuration (#458)
Browse files Browse the repository at this point in the history
* allow option configuration
* add upgrade notes
* allow form assembling without request and view resolver
* add messages to submission event, allow to disable flash bag
* fix action and flash message getter
* omit form name in headless build
* add is_headless_form option
* add form dialog builder
* add docs
  • Loading branch information
solverat committed Jul 11, 2024
1 parent e0eb24e commit 0be1a0c
Show file tree
Hide file tree
Showing 23 changed files with 776 additions and 333 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

```json
"require" : {
"dachcom-digital/formbuilder" : "~5.0.0"
"dachcom-digital/formbuilder" : "~5.1.0"
}
```

Expand Down Expand Up @@ -52,6 +52,7 @@ Nothing to tell here, it's just [Symfony](https://symfony.com/doc/current/templa

## Further Information
- [Usage (Rendering Types, Configuration)](docs/0_Usage.md)
- [Headless Mode](docs/1_HeadlessMode.md)
- [SPAM Protection (Honeypot, reCAPTCHA)](docs/03_SpamProtection.md)
- [Output Workflows](docs/OutputWorkflow/0_Usage.md)
- [API Channel](docs/OutputWorkflow/09_ApiChannel.md)
Expand Down
6 changes: 6 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
- **[SECURITY FEATURE]** Add [friendly captcha field](/docs/03_SpamProtection.md#friendly-captcha)
- **[SECURITY FEATURE]** Add [cloudflare turnstile](/docs/03_SpamProtection.md#cloudflare-turnstile)
- **[BUGFIX]** Use Pimcore AdminUserTranslator for Editable Dialog Box [#450](https://github.com/dachcom-digital/pimcore-formbuilder/issues/450)
- **[IMPROVEMENT]** Improve json response success message behaviour [#416](https://github.com/dachcom-digital/pimcore-formbuilder/issues/416)
- **[IMPROVEMENT]** [#458](https://github.com/dachcom-digital/pimcore-formbuilder/pull/458)
- Allow to modify FormType options via `FORM_TYPE_OPTIONS` event
- Do not render `formRuntimeDataToken` if csrf has been disabled in form options
- Allow form assembling without request and view resolver
- Add FormDialogBuilder

## 5.0.7
- Remove `editable_root` restriction from mail editor
Expand Down
5 changes: 3 additions & 2 deletions config/services/brick.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ services:
autoconfigure: true
public: true

# area brick
FormBuilderBundle\Document\Areabrick\Form\Form:
FormBuilderBundle\Document\Areabrick\Form\FormDialogBuilder:
arguments:
$translator: '@Pimcore\Bundle\AdminBundle\Translation\AdminUserTranslator'

FormBuilderBundle\Document\Areabrick\Form\Form:
tags:
- { name: pimcore.area.brick, id: formbuilder_form }
1 change: 1 addition & 0 deletions config/services/forms/forms.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ services:
arguments:
- '@Symfony\Component\Form\FormFactoryInterface'
- '@FormBuilderBundle\Configuration\Configuration'
- '@event_dispatcher'
- '@FormBuilderBundle\Registry\DynamicMultiFileAdapterRegistry'
tags:
- { name: form.type }
Expand Down
21 changes: 13 additions & 8 deletions docs/0_Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
There are **three** ways to render your form.
> **Important:** It's possible to use multiple forms per page but **never** render the same form twice on the same page!
## Headless Mode
If you want to use FormBuilder in headless mode, please check out the documentation [here](./1_HeadlessMode.md).

***

## Usage I. Area Brick
This is the most used one. Just place a form element (Area Brick) somewhere on your document.
Configure it via the available edit button.
Expand All @@ -10,14 +15,14 @@ Configure it via the available edit button.
Before we start, check out the available options.
Those are (only) needed for the twig and controller rendering type.

| Name | Description |
|------|-------------|
| `form_id` | Can you guess it? It's the Form Id, right. |
| `form_template` | Form Template, for example: `bootstrap_4_layout.html.twig` |
| `main_layout` | This option is only needed if you render a form via a controller. By default, FormBuilder extends a empty default layout. If you want do extend your custom layout, define it: `layout.html.twig` |
| `preset` | Optional: set a custom preset |
| `custom_options` | Optional (array): Add some custom options as array here to pass them through the whole submission process (available in SubmissionEvent for example |
| `output_workflow` | Define, which output workflow should get dispatched after a form has been successfully submitted. You could use the ID or Name of a output workflow |
| Name | Description |
|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `form_id` | Can you guess it? It's the Form Id, right. |
| `form_template` | Form Template, for example: `bootstrap_4_layout.html.twig` |
| `main_layout` | This option is only needed if you render a form via a controller. By default, FormBuilder extends a empty default layout. If you want do extend your custom layout, define it: `layout.html.twig` |
| `preset` | Optional: set a custom preset |
| `custom_options` | Optional (array): Add some custom options as array here to pass them through the whole submission process (available in SubmissionEvent for example |
| `output_workflow` | Define, which output workflow should get dispatched after a form has been successfully submitted. You could use the ID or Name of a output workflow |

## Usage II. Twig
Create a Form using the Twig Extension.
Expand Down
61 changes: 61 additions & 0 deletions docs/1_HeadlessMode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Headless Mode

## AreaBrick
You need to create your own editable, depending on your integration. You may want to use the `FormDialogBuilder` to simplify the area configuration section.

## Form Building
This is an example, how you may want to build FormBuilder forms in headless mode:

```php
$optionBuilder = new FormOptionsResolver();
$optionBuilder->setFormId($formId);
$optionBuilder->setFormTemplate($formTemplate);
$optionBuilder->setFormPreset($formPreset);
$optionBuilder->setOutputWorkflow($formOutputWorkflow);

$form = $this->formAssembler->assembleHeadlessForm($optionBuilder);

$form->submit($request->request->all());

if (!$form->isValid()) {

// process validation errors here

return;
}

$response = null;
$submissionEvent = new SubmissionEvent($request, $formRuntimeData, $form, null, false);
$this->eventDispatcher->dispatch($submissionEvent, FormBuilderEvents::FORM_SUBMIT_SUCCESS);

$finishResponse = $this->formSubmissionFinisher->finishWithSuccess($request, $submissionEvent);

if ($finishResponse instanceof RedirectResponse) {
return [
'success' => true,
'redirect' => $finishResponse->getTargetUrl()
];
}

if ($finishResponse instanceof JsonResponse) {
$response = json_decode($finishResponse->getContent(), true, 512, JSON_THROW_ON_ERROR);
}

if ($response === null) {
throw new \InvalidArgumentException('empty success response');
}

if ($response['success'] === false) {

if (!array_key_exists('validation_errors', $response)) {
return $response;
}

$validationErrors = $response['validation_errors']
// process validation errors here

return null;
}

return $response;
```
17 changes: 17 additions & 0 deletions docs/70_Events.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

It's possible to add some events to every form submission.

## Form Type Options Event
The `FORM_TYPE_OPTIONS` event is dispatched after the form builder options has been defined.
It contains the field name, type and the defined options. You're able to modify the options only.

@see \FormBuilderBundle\Event\Form\FormTypeOptionsEvent

**Example**
```php
<?php

use FormBuilderBundle\FormBuilderEvents;

[
FormBuilderEvents::FORM_TYPE_OPTIONS => 'formTypeOptions'
];
```

## Pre Set Data Event
The `FORM_PRE_SET_DATA` event is dispatched at the beginning of the Form::setData() method.
It contains the form event and also some form builder settings.
Expand Down
160 changes: 118 additions & 42 deletions src/Assembler/FormAssembler.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use FormBuilderBundle\Resolver\FormOptionsResolver;
use FormBuilderBundle\Manager\FormDefinitionManager;
use FormBuilderBundle\Model\FormDefinitionInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class FormAssembler
Expand All @@ -21,74 +22,149 @@ public function __construct(
) {
}

/**
* @throws \Exception
*/
public function assemble(FormOptionsResolver $optionsResolver): array
{
return $this->assembleViewVars($optionsResolver);
try {
$formDefinition = $this->getFormDefinition($optionsResolver);
} catch (\Throwable $e) {
return [
'message' => $e->getMessage(),
'form_layout' => $optionsResolver->getFormLayout(),
'form_template' => null,
'form_id' => null
];
}

$formAssembleEvent = $this->dispatchAssembleEvent(
FormBuilderEvents::FORM_ASSEMBLE_PRE,
[$optionsResolver, $formDefinition]
);

$viewVars = $this->getViewVars($optionsResolver);

$form = $this->buildForm(
$formDefinition,
$optionsResolver,
$formAssembleEvent->getFormData(),
);

$this->dispatchAssembleEvent(
FormBuilderEvents::FORM_ASSEMBLE_POST,
[$optionsResolver, $formDefinition, $form]
);

$viewVars['form'] = $form->createView();

return $viewVars;
}

public function assembleViewVars(FormOptionsResolver $optionsResolver): array
/**
* @throws \Exception
*/
public function assembleHeadlessForm(FormOptionsResolver $optionsResolver): FormInterface
{
$builderError = false;
$exceptionMessage = null;
$formDefinition = null;
$formDefinition = $this->getFormDefinition($optionsResolver);

$formAssembleEvent = $this->dispatchAssembleEvent(
FormBuilderEvents::FORM_ASSEMBLE_PRE,
[$optionsResolver, $formDefinition]
);

$form = $this->buildForm(
$formDefinition,
$optionsResolver,
$formAssembleEvent->getFormData(),
true
);

$this->dispatchAssembleEvent(
FormBuilderEvents::FORM_ASSEMBLE_POST,
[$optionsResolver, $formDefinition, $form]
);

return $form;
}

/**
* @throws \Exception
*/
public function getFormDefinition(FormOptionsResolver $optionsResolver): FormDefinitionInterface
{
$formId = $optionsResolver->getFormId();

if ($formId !== null) {
try {
$formDefinition = $this->formDefinitionManager->getById($formId);
if (!$formDefinition instanceof FormDefinitionInterface) {
$exceptionMessage = sprintf('Form with id "%s" is not valid.', $formId);
$builderError = true;
}
} catch (\Exception $e) {
$exceptionMessage = $e->getMessage();
$builderError = true;
}
} else {
$exceptionMessage = 'No valid form selected.';
$builderError = true;
if ($formId === null) {
throw new \Exception('No valid form selected.', 404);
}

$viewVars = [];
$viewVars['form_layout'] = $optionsResolver->getFormLayout();
$formDefinition = $this->formDefinitionManager->getById($formId);
if (!$formDefinition instanceof FormDefinitionInterface) {
throw new \Exception(sprintf('Form with id "%s" is not valid.', $formId), 404);
}

return $formDefinition;
}

/**
* @throws \Exception
*/
public function buildForm(
FormDefinitionInterface $formDefinition,
FormOptionsResolver $optionsResolver,
array $formData = [],
bool $headless = false
): FormInterface {

$systemRuntimeData = $this->getSystemRuntimeData($optionsResolver, $headless);
$formAttributes = $optionsResolver->getFormAttributes();
$useCsrfProtection = $optionsResolver->useCsrfProtection();

if ($builderError === true) {
$viewVars['message'] = $exceptionMessage;
$viewVars['form_template'] = null;
$viewVars['form_id'] = null;
$formRuntimeDataCollector = $this->formRuntimeDataAllocator->allocate($formDefinition, $systemRuntimeData);
$formRuntimeData = $formRuntimeDataCollector->getData();

return $viewVars;
if ($headless === true) {
return $this->frontendFormBuilder->buildHeadlessForm($formDefinition, $formRuntimeData, $formAttributes, $formData, $useCsrfProtection);
}

$formAssembleEvent = new FormAssembleEvent($optionsResolver, $formDefinition);
$this->eventDispatcher->dispatch($formAssembleEvent, FormBuilderEvents::FORM_ASSEMBLE_PRE);
return $this->frontendFormBuilder->buildForm($formDefinition, $formRuntimeData, $formAttributes, $formData, $useCsrfProtection);
}

$systemRuntimeData = [
'form_preset' => $optionsResolver->getFormPreset(),
'form_output_workflow' => $optionsResolver->getOutputWorkflow(),
public function getSystemRuntimeData(FormOptionsResolver $optionsResolver, bool $headless = false): array
{
$data = [
'form_preset' => $optionsResolver->getFormPreset(),
'form_output_workflow' => $optionsResolver->getOutputWorkflow(),
'custom_options' => $optionsResolver->getCustomOptions()
];

return $headless ? $data : array_merge($data, [
'form_template' => $optionsResolver->getFormTemplateName(),
'form_template_full_path' => $optionsResolver->getFormTemplate(),
'custom_options' => $optionsResolver->getCustomOptions()
];
]);
}

public function getViewVars(FormOptionsResolver $optionsResolver): array
{
$viewVars = [];

$viewVars['form_layout'] = $optionsResolver->getFormLayout();
$viewVars['form_block_template'] = $optionsResolver->getFormBlockTemplate();
$viewVars['form_template'] = $optionsResolver->getFormTemplate();
$viewVars['form_id'] = $optionsResolver->getFormId();
$viewVars['form_preset'] = $optionsResolver->getFormPreset();
$viewVars['form_output_workflow'] = $optionsResolver->getOutputWorkflow();
$viewVars['main_layout'] = $optionsResolver->getMainLayout();

$formRuntimeDataCollector = $this->formRuntimeDataAllocator->allocate($formDefinition, $systemRuntimeData);
$formRuntimeData = $formRuntimeDataCollector->getData();

$form = $this->frontendFormBuilder->buildForm($formDefinition, $formRuntimeData, $formAssembleEvent->getFormData());

$formAssembleEvent = new FormAssembleEvent($optionsResolver, $formDefinition, $form);
$this->eventDispatcher->dispatch($formAssembleEvent, FormBuilderEvents::FORM_ASSEMBLE_POST);
return $viewVars;
}

$viewVars['form'] = $form->createView();
public function dispatchAssembleEvent(string $eventName, array $arguments): FormAssembleEvent
{
$formAssembleEvent = new FormAssembleEvent(...$arguments);
$this->eventDispatcher->dispatch($formAssembleEvent, $eventName);

return $viewVars;
return $formAssembleEvent;
}
}
Loading

0 comments on commit 0be1a0c

Please sign in to comment.