Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce channel context, resolves #486 #504

Merged
merged 4 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading