Skip to content

Commit

Permalink
introduce channel context, resolves #486 (#504)
Browse files Browse the repository at this point in the history
  • Loading branch information
solverat authored Jan 13, 2025
1 parent d24810e commit dceb991
Show file tree
Hide file tree
Showing 19 changed files with 310 additions and 81 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Nothing to tell here, it's just [Symfony](https://symfony.com/doc/current/templa
- [Email Channel](docs/OutputWorkflow/10_EmailChannel.md)
- [Object Channel](docs/OutputWorkflow/11_ObjectChannel.md)
- [Custom Channel](docs/OutputWorkflow/12_CustomChannel.md)
- [Channel Context](docs/OutputWorkflow/13_ChannelContext.md)
- [Output Transformer](docs/OutputWorkflow/15_OutputTransformer.md)
- [Field Transformer](docs/OutputWorkflow/16_FieldTransformer.md)
- [Success Management](docs/OutputWorkflow/20_SuccessManagement.md)
Expand Down
8 changes: 6 additions & 2 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Upgrade Notes

## 5.1.5
- **[BUGFIX]** Update custom channel docs [#493](https://github.com/dachcom-digital/pimcore-formbuilder/issues/493)
## 5.2.0
- **[NEW FEATURE]** Bootstrap 5 Layout Support [@mackrais](https://github.com/dachcom-digital/pimcore-formbuilder/pull/500)
- **[NEW FEATURE]** Introduce Channel Context [#486](https://github.com/dachcom-digital/pimcore-formbuilder/issues/486)
- **[IMPROVEMENT]** Doctrine ORM 3.0 Support [#503](https://github.com/dachcom-digital/pimcore-formbuilder/pull/503)
- **[BUGFIX]** API Channel: Keep array index when merging child nodes [@simon-matt-oetztal](https://github.com/dachcom-digital/pimcore-formbuilder/pull/496)
- **[BUGFIX]** Update Custom Channel Documentation [#493](https://github.com/dachcom-digital/pimcore-formbuilder/issues/493)

## 5.1.4
- **[BUGFIX]** Allow using double-opt-in variables in placeholder processor
Expand Down
5 changes: 3 additions & 2 deletions docs/OutputWorkflow/09_ApiChannel.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class MailChimpApiProvider implements ApiProviderInterface
}
}

protected function getClient()
protected function getClient(): MailchimpMarketing\ApiClient
{
$mailchimp = new MailchimpMarketing\ApiClient();

Expand Down Expand Up @@ -213,12 +213,13 @@ class OutputWorkflowEventListener implements EventSubscriberInterface
// different fail scenarios can be applied:

$event->shouldFail('My invalid message for a specific channel! Allow further channels to pass!', true);

// OR
$event->shouldFail('My invalid message! If this happens, no further channel will be executed!', false);

// silently skip channel
if ($subject->getProviderConfigurationNode('myConfig') === 'a special value') {
$event->shouldSuspend();

return;
}
}
Expand Down
4 changes: 3 additions & 1 deletion docs/OutputWorkflow/12_CustomChannel.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ services:
## Output Transformer
Read [here](./15_OutputTransformer.md#custom-output-transformer) how to add a single output transformer to your new custom channel.
## Channel Context
Read [here](./13_ChannelContext.md) how to support channel context within your new custom channel.
## PHP Configuration Form Type Class
```php
<?php
Expand Down Expand Up @@ -122,5 +125,4 @@ Formbuilder.extjs.formPanel.outputWorkflow.channel.myChannel = Class.create(Form
return this.panel.form.getValues();
}
});

```
34 changes: 34 additions & 0 deletions docs/OutputWorkflow/13_ChannelContext.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Channel Context

The `ChannelContext` object stores data within a full workflow livecycle.
If you need to pass data from one channel to another, this object comes in handy!

All core Channels (Email, Object, API) receives the `FormBuilderBundle\OutputWorkflow\Channel\ChannelContext` object

## Channel Context and Signal Storage
The Channel Context is also available in Signal Storage (`OutputWorkflowSignalsEvent->getContextItem('channelContext)`).
This can be helpful, if you need to clean up data after an exception occurs or the workflow has been completely processed.

## Channel Context in Custom Channel
To receive the channel context in a custom channel, you need to implement the `ChannelContextAwareInterface`:

> [!IMPORTANT]
> If your channel also dispatches a `ChannelSubjectGuardEvent`,
> don't forget to pass the channel context to it to pass the context to upcoming channels!
If your channel implements the `ChannelContextAwareInterface`, the `OutputWorkflowDispatcher` automatically will inject the active `ChannelContext` object.

```php
<?php

namespace FormBuilderBundle\OutputWorkflow\Channel\Email;

use FormBuilderBundle\OutputWorkflow\Channel\ChannelInterface;
use FormBuilderBundle\OutputWorkflow\Channel\ChannelContextAwareInterface;
use FormBuilderBundle\OutputWorkflow\Channel\Trait\ChannelContextTrait;

class EmailOutputChannel implements ChannelInterface, ChannelContextAwareInterface
{
use ChannelContextTrait;
}
```
32 changes: 26 additions & 6 deletions docs/OutputWorkflow/30_Events.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,51 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class OutputWorkflowEventListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
public static function getSubscribedEvents(): array
{
return [
FormBuilderEvents::OUTPUT_WORKFLOW_GUARD_SUBJECT_PRE_DISPATCH => 'checkSubject',
FormBuilderEvents::OUTPUT_WORKFLOW_GUARD_SUBJECT_PRE_DISPATCH => ['addChannelContextData', 'checkSubject'],
];
}

public function checkSubject(ChannelSubjectGuardEvent $event)
public function addChannelContextData(ChannelSubjectGuardEvent $event): void
{
// this could be a data object but also a field collection
$subject = $event->getSubject();

if($event->getWorkflowName() === 'my_weird_workflow') {
if (
// if it's a special channel, add some context data to fetch it again in the next channel!
$event->getWorkflowName() === 'my_workflow' &&
$event->getChannelType() === 'mail' &&
$event->hasChannelContext()
) {
$event
->getChannelContext()
->addContextData('special_key', 'my_special_data');
}
}

public function checkSubject(ChannelSubjectGuardEvent $event): void
{
// this could be a data object but also a field collection
$subject = $event->getSubject();

if ($event->getWorkflowName() === 'my_workflow') {
$event->shouldFail('My invalid message for a specific channel! Allow further channels to pass!', true);

return;
}

if($event->getWorkflowName() === 'my_second_weird_workflow') {
if ($event->getWorkflowName() === 'my_second_workflow') {
$event->shouldFail('My invalid message! If this happens, no further channel will be executed!', false);

return;
}

if($event->getChannelType() === 'object') {
if ($event->getChannelType() === 'object') {
// silently skip channel
$event->shouldSuspend();

return;
}
}
Expand Down
18 changes: 17 additions & 1 deletion src/Event/OutputWorkflow/ChannelSubjectGuardEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace FormBuilderBundle\Event\OutputWorkflow;

use FormBuilderBundle\Form\Data\FormDataInterface;
use FormBuilderBundle\OutputWorkflow\Channel\ChannelContext;
use Symfony\Contracts\EventDispatcher\Event;

class ChannelSubjectGuardEvent extends Event
Expand All @@ -17,7 +18,8 @@ public function __construct(
protected mixed $subject,
protected string $workflowName,
protected string $channelType,
protected array $formRuntimeData
protected array $formRuntimeData,
protected ?ChannelContext $channelContext = null
) {
}

Expand Down Expand Up @@ -51,6 +53,20 @@ public function getChannelType(): string
return $this->channelType;
}

public function hasChannelContext(): bool
{
return $this->channelContext instanceof ChannelContext;
}

public function getChannelContext(): ChannelContext
{
if (!$this->hasChannelContext()) {
throw new \RuntimeException('ChannelContext not available');
}

return $this->channelContext;
}

/**
* Silently suspend current process without any notices.
*/
Expand Down
41 changes: 22 additions & 19 deletions src/OutputWorkflow/Channel/Api/ApiData.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,20 @@

namespace FormBuilderBundle\OutputWorkflow\Channel\Api;

use FormBuilderBundle\OutputWorkflow\Channel\ChannelContext;
use Symfony\Component\Form\FormInterface;

class ApiData
{
protected string $apiProviderName;
protected array $apiNodes;
protected ?array $providerConfiguration;
protected string $locale;
protected array $formRuntimeData;
protected FormInterface $form;

public function __construct(
string $apiProviderName,
array $apiNodes,
?array $providerConfiguration,
string $locale,
array $formRuntimeData,
FormInterface $form
protected string $apiProviderName,
protected array $apiNodes,
protected ?array $providerConfiguration,
protected string $locale,
protected array $formRuntimeData,
protected FormInterface $form,
protected ?ChannelContext $channelContext
) {
$this->apiProviderName = $apiProviderName;
$this->apiNodes = $apiNodes;
$this->providerConfiguration = $providerConfiguration;
$this->locale = $locale;
$this->formRuntimeData = $formRuntimeData;
$this->form = $form;
}

public function getApiProviderName(): string
Expand All @@ -39,6 +28,20 @@ public function getForm(): FormInterface
return $this->form;
}

public function hasChannelContext(): bool
{
return $this->channelContext instanceof ChannelContext;
}

public function getChannelContext(): ChannelContext
{
if (!$this->hasChannelContext()) {
throw new \RuntimeException('ChannelContext not available');
}

return $this->channelContext;
}

public function getFormRuntimeData(): array
{
return $this->formRuntimeData;
Expand Down
18 changes: 16 additions & 2 deletions src/OutputWorkflow/Channel/Api/ApiOutputChannel.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

use FormBuilderBundle\Event\SubmissionEvent;
use FormBuilderBundle\Form\Admin\Type\OutputWorkflow\Channel\ApiChannelType;
use FormBuilderBundle\OutputWorkflow\Channel\ChannelContextAwareInterface;
use FormBuilderBundle\OutputWorkflow\Channel\ChannelInterface;
use FormBuilderBundle\OutputWorkflow\Channel\Trait\ChannelContextTrait;

class ApiOutputChannel implements ChannelInterface
class ApiOutputChannel implements ChannelInterface, ChannelContextAwareInterface
{
use ChannelContextTrait;

public function __construct(protected ApiOutputChannelWorker $apiOutputChannelWorker)
{
}
Expand All @@ -33,7 +37,17 @@ public function getUsedFormFieldNames(array $channelConfiguration): array

public function dispatchOutputProcessing(SubmissionEvent $submissionEvent, string $workflowName, array $channelConfiguration): void
{
$this->apiOutputChannelWorker->process($submissionEvent, $workflowName, $channelConfiguration);
$locale = $submissionEvent->getLocale() ?? $submissionEvent->getRequest()->getLocale();
$form = $submissionEvent->getForm();
$formRuntimeData = $submissionEvent->getFormRuntimeData();

$context = [
'locale' => $locale,
'doubleOptInSession' => $submissionEvent->getDoubleOptInSession(),
'channelContext' => $this->getChannelContext(),
];

$this->apiOutputChannelWorker->process($form, $channelConfiguration, $formRuntimeData, $workflowName, $context);
}

protected function findUsedFormFieldsInConfiguration(array $definitionFields, array $fieldNames = []): array
Expand Down
39 changes: 27 additions & 12 deletions src/OutputWorkflow/Channel/Api/ApiOutputChannelWorker.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
namespace FormBuilderBundle\OutputWorkflow\Channel\Api;

use FormBuilderBundle\Event\OutputWorkflow\ChannelSubjectGuardEvent;
use FormBuilderBundle\Event\SubmissionEvent;
use FormBuilderBundle\Exception\OutputWorkflow\GuardChannelException;
use FormBuilderBundle\Exception\OutputWorkflow\GuardOutputWorkflowException;
use FormBuilderBundle\Form\FormValuesOutputApplierInterface;
use FormBuilderBundle\FormBuilderEvents;
use FormBuilderBundle\OutputWorkflow\Channel\ChannelContext;
use FormBuilderBundle\Registry\ApiProviderRegistry;
use FormBuilderBundle\Registry\FieldTransformerRegistry;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
Expand All @@ -23,16 +23,15 @@ public function __construct(
) {
}

public function process(SubmissionEvent $submissionEvent, string $workflowName, array $channelConfiguration): void
public function process(FormInterface $form, array $channelConfiguration, array $formRuntimeData, string $workflowName, array $context = []): void
{
$formRuntimeData = $submissionEvent->getFormRuntimeData();
$locale = $submissionEvent->getRequest()->getLocale();
$form = $submissionEvent->getForm();

$apiProviderName = $channelConfiguration['apiProvider'];
$apiMappingData = $channelConfiguration['apiMappingData'];
$providerConfiguration = $channelConfiguration['apiConfiguration'];

$channelContext = $context['channelContext'] ?? null;
$locale = $context['locale'] ?? null;

// no data, no gain.
if (!is_array($apiMappingData)) {
return;
Expand All @@ -50,9 +49,9 @@ public function process(SubmissionEvent $submissionEvent, string $workflowName,
return;
}

$apiData = new ApiData($apiProviderName, $nodes, $providerConfiguration, $locale, $formRuntimeData, $form);
$apiData = new ApiData($apiProviderName, $nodes, $providerConfiguration, $locale, $formRuntimeData, $form, $channelContext);

if (null === $apiData = $this->dispatchGuardEvent($apiData, $form, $workflowName, $formRuntimeData)) {
if (null === $apiData = $this->dispatchGuardEvent($apiData, $form, $workflowName, $formRuntimeData, $channelContext)) {
return;
}

Expand Down Expand Up @@ -248,9 +247,23 @@ protected function findFormDataField(string $requestedFieldName, array $data): ?
* @throws GuardChannelException
* @throws GuardOutputWorkflowException
*/
protected function dispatchGuardEvent($subject, FormInterface $form, string $workflowName, array $formRuntimeData): mixed
{
$channelSubjectGuardEvent = new ChannelSubjectGuardEvent($form->getData(), $subject, $workflowName, 'api', $formRuntimeData);
protected function dispatchGuardEvent(
$subject,
FormInterface $form,
string $workflowName,
array $formRuntimeData,
?ChannelContext $channelContext
): mixed {

$channelSubjectGuardEvent = new ChannelSubjectGuardEvent(
$form->getData(),
$subject,
$workflowName,
'api',
$formRuntimeData,
$channelContext
);

$this->eventDispatcher->dispatch($channelSubjectGuardEvent, FormBuilderEvents::OUTPUT_WORKFLOW_GUARD_SUBJECT_PRE_DISPATCH);

if ($channelSubjectGuardEvent->isSuspended()) {
Expand All @@ -259,7 +272,9 @@ protected function dispatchGuardEvent($subject, FormInterface $form, string $wor

if ($channelSubjectGuardEvent->shouldStopChannel()) {
throw new GuardChannelException($channelSubjectGuardEvent->getFailMessage());
} elseif ($channelSubjectGuardEvent->shouldStopOutputWorkflow()) {
}

if ($channelSubjectGuardEvent->shouldStopOutputWorkflow()) {
throw new GuardOutputWorkflowException($channelSubjectGuardEvent->getFailMessage());
}

Expand Down
Loading

0 comments on commit dceb991

Please sign in to comment.