Skip to content

Commit 4cab9fc

Browse files
authored
Port API Outputchannel & Field Transformer (#318)
* implement API channel, resolves #301 * implement field transformer * add px adjusments
1 parent 2cca0b3 commit 4cab9fc

File tree

36 files changed

+2308
-9
lines changed

36 files changed

+2308
-9
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
| Release | Supported Pimcore Versions | Supported Symfony Versions | Release Date | Maintained | Branch |
1414
|---------|-----------------------------------|----------------------------|--------------|----------------|------------|
15-
| **4.x** | `10.1` - `10.2` | `5.3` | 13.10.2021 | Feature Branch | master |
15+
| **4.x** | `10.1` - `10.3` | `5.3` | 13.10.2021 | Feature Branch | master |
1616
| **3.x** | `6.0` - `6.9` | `3.4`, `^4.4` | 17.07.2019 | Bugfix only | [3.x](https://github.com/dachcom-digital/pimcore-formbuilder/tree/3.x) |
1717
| **2.7** | `5.4`, `5.5`, `5.6`, `5.7`, `5.8` | `3.4` | 27.06.2019 | Unsupported | [2.7](https://github.com/dachcom-digital/pimcore-formbuilder/tree/2.7) |
1818
| **1.5** | `4.0` | -- | 18.03.2017 | Unsupported | [pimcore4](https://github.com/dachcom-digital/pimcore-formbuilder/tree/pimcore4) |
@@ -47,10 +47,13 @@ Nothing to tell here, it's just [Symfony](https://symfony.com/doc/current/templa
4747
- [SPAM Protection (Honeypot, reCAPTCHA)](docs/03_SpamProtection.md)
4848
- [Usage (Rendering Types, Configuration)](docs/0_Usage.md)
4949
- [Output Workflows](docs/OutputWorkflow/0_Usage.md)
50+
- [Output Workflows](docs/OutputWorkflow/0_Usage.md)
51+
- [API Channel](docs/OutputWorkflow/09_ApiChannel.md)
5052
- [Email Channel](docs/OutputWorkflow/10_EmailChannel.md)
5153
- [Object Channel](docs/OutputWorkflow/11_ObjectChannel.md)
5254
- [Custom Channel](docs/OutputWorkflow/12_CustomChannel.md)
5355
- [Output Transformer](docs/OutputWorkflow/15_OutputTransformer.md)
56+
- [Field Transformer](docs/OutputWorkflow/16_FieldTransformer.md)
5457
- [Success Management](docs/OutputWorkflow/20_SuccessManagement.md)
5558
- [Backend Administration of Forms](docs/01_BackendUsage.md)
5659
- [Export Forms](docs/02_ExportForms.md)

UPGRADE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Upgrade Notes
22

3+
#### Version 4.1.0
4+
- **[NEW FEATURE]**: API Output channel [#290](https://github.com/dachcom-digital/pimcore-formbuilder/issues/301)
5+
- **[NEW FEATURE]**: API Output channel Field Transformer
6+
37
## Version 4.0.2
48
- [ENHANCEMENT] enable placeholder in cc and bcc field in email output workflow [@frithjof](https://github.com/dachcom-digital/pimcore-formbuilder/pull/305)
59

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
# API Channel
2+
![image](https://user-images.githubusercontent.com/700119/145599712-37b8468e-975e-4f3e-9fd5-a82dd76e3c53.png)
3+
4+
Use the mail channel to submit structured to any kind of API you want.
5+
6+
- [Mapping](./09_ApiChannel.md#mapping) | Map form fields to predefined api fields
7+
- [API Provider](./09_ApiChannel.md#api-provider) | Create a custom api provider
8+
- [Guard Event Listener](./09_ApiChannel.md#guard-event-listener) | Hook into api provider process
9+
- [Code Example: Trigger Value](./09_ApiChannel.md#example-trigger-value) | Dispatch Channel by given trigger
10+
- [Field Transformer](./16_FieldTransformer.md) *(Optional)* | Use a field transformer to modify single api field values
11+
12+
## Note
13+
The FormBuilder API Channel does **not** ship preconfigured API Provider. They can be simple but also complex. But no worries,
14+
it's quite easy to integrate your own api provider, read more about it [here](./09_ApiChannel.md#api-provider)
15+
16+
## Available Options
17+
18+
| Name | Type | Description |
19+
|------|-------------|-------------|
20+
| Api Provider| `ApiProviderInterface` | Select your API Provider. |
21+
| Options | `mixed` | If available, various provider configuration fields |
22+
23+
## Mapping
24+
![image](https://user-images.githubusercontent.com/700119/145618709-686d5022-1ed9-4722-9600-0d41eccf55a3.png)
25+
26+
If the API Provider supports predefined API fields, you're able to map form fields to these fields which will show up in a
27+
dropdown element. If there are no predefined API fields, you need to enter them manually.
28+
29+
### Container "Fieldset" Mapping
30+
If an API field is assigned to the fieldset itself, the child elements will be created as an array branch. Otherwise, child
31+
elements will be assigned flat.
32+
33+
### Container "Repeater" Mapping
34+
If no API field is assigned to the repeater itself, the child elements will be skipped.
35+
36+
***
37+
38+
## API Provider
39+
Integrating an api provider is very simple. In this example, we're going to set up an API provider for MailChimp.
40+
41+
### API Configuration Fields
42+
Every API Provider is allowed to provide custom configuration fields (see example below). This allows you to select various data
43+
for each form (like a campaign ID in MailChimp)
44+
45+
### API Predefined Fields
46+
If the API Provider returns predefined you **must** map your form fields with these given fields.
47+
48+
> Note! You're allowed to add a predefined only once, but you're allowed to add multiple predefined properties to a single form field!
49+
50+
```bash
51+
$ composer require mailchimp/marketing
52+
```
53+
54+
First, we need to register a new service:
55+
```yml
56+
AppBundle\FormBuilder\ApiProvider\MailChimpApiProvider:
57+
autowire: true
58+
public: false
59+
tags:
60+
- { name: form_builder.api.provider, identifier: mailchimp }
61+
```
62+
63+
Then, we're going to implement the service itself:
64+
```php
65+
<?php
66+
67+
namespace AppBundle\FormBuilder\ApiProvider;
68+
69+
use FormBuilderBundle\Model\FormDefinitionInterface;
70+
use FormBuilderBundle\OutputWorkflow\Channel\Api\ApiData;
71+
use FormBuilderBundle\OutputWorkflow\Channel\Api\ApiProviderInterface;
72+
73+
class MailChimpApiProvider implements ApiProviderInterface
74+
{
75+
public function getName(): string
76+
{
77+
return 'MailChimp';
78+
}
79+
80+
public function getProviderConfigurationFields(FormDefinitionInterface $formDefinition): array
81+
{
82+
$mailchimp = $this->getClient();
83+
$campaignsData = $mailchimp->campaigns->list();
84+
85+
$campaignStore = [];
86+
foreach ($campaignsData->campaigns as $campaign) {
87+
$campaignStore[] = [
88+
'value' => $campaign->id,
89+
'label' => $campaign->settings->title
90+
];
91+
}
92+
93+
return [
94+
[
95+
'type' => 'text',
96+
'label' => 'My Config',
97+
'name' => 'myConfig',
98+
'required' => true,
99+
],
100+
[
101+
'type' => 'select',
102+
'label' => 'Campaign',
103+
'name' => 'campaign',
104+
'store' => $campaignStore,
105+
'required' => true,
106+
]
107+
];
108+
}
109+
110+
public function getPredefinedApiFields(FormDefinitionInterface $formDefinition, array $providerConfiguration): array
111+
{
112+
// maybe they will come from a remote campaign list.
113+
// just return an empty array if you don't want to provide predefined api fields.
114+
115+
$fields = [
116+
'EMAIL',
117+
'MMERGE6',
118+
'FNAME',
119+
'LNAME'
120+
];
121+
122+
if ($providerConfiguration['campaign'] === '123') {
123+
$fields[] = 'SPECIAL_FIELD';
124+
}
125+
126+
return $fields;
127+
}
128+
129+
public function process(ApiData $apiData): void
130+
{
131+
$mailchimp = $this->getClient();
132+
$campaignId = $apiData->getProviderConfigurationNode('campaign');
133+
134+
$campaigns = $mailchimp->campaigns->get($campaignId);
135+
136+
$body = [
137+
'status' => 'subscribed',
138+
'email_address' => $apiData->getApiNode('EMAIL'),
139+
'merge_fields' => $apiData->getApiNodes()
140+
];
141+
142+
try {
143+
$mailchimp->lists->addListMember($campaignId, $body);
144+
} catch(\Throwable $e) {
145+
146+
// don't forget to wrap your remote calls in a correct exception:
147+
148+
// I. fail silently. no error will be visible to the user
149+
150+
// II. OR: only channel should get bypassed (upcoming channels will be processed)
151+
// error will be visible to the user after workflow has been completely dispatched
152+
throw new GuardChannelException($e->getMessage());
153+
154+
// III. OR: workflow should not go one. cancel from now on
155+
// error will be visible to the user after workflow has been completely dispatched
156+
throw new GuardOutputWorkflowException($e->getMessage());
157+
}
158+
}
159+
160+
protected function getClient()
161+
{
162+
$mailchimp = new MailchimpMarketing\ApiClient();
163+
164+
$mailchimp->setConfig([
165+
'apiKey' => 'YOUR_API_KEY',
166+
'server' => 'YOUR_SERVER_PREFIX'
167+
]);
168+
169+
return $mailchimp;
170+
}
171+
}
172+
```
173+
174+
***
175+
176+
## Guard Event Listener
177+
As in every output channel, you're able to hook into the dispatch event via `OUTPUT_WORKFLOW_GUARD_SUBJECT_PRE_DISPATCH` event.
178+
179+
```php
180+
<?php
181+
182+
namespace AppBundle\FormBuilder;
183+
184+
use FormBuilderBundle\FormBuilderEvents;
185+
use FormBuilderBundle\Event\OutputWorkflow\ChannelSubjectGuardEvent;
186+
use FormBuilderBundle\OutputWorkflow\Channel\Api\ApiData;
187+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
188+
use AppBundle\FormBuilder\ApiProvider\MailChimpApiProvider;
189+
190+
class OutputWorkflowEventListener implements EventSubscriberInterface
191+
{
192+
public static function getSubscribedEvents(): array
193+
{
194+
return [
195+
FormBuilderEvents::OUTPUT_WORKFLOW_GUARD_SUBJECT_PRE_DISPATCH => 'checkSubject',
196+
];
197+
}
198+
199+
public function checkSubject(ChannelSubjectGuardEvent $event): void
200+
{
201+
$subject = $event->getSubject();
202+
203+
// only apply if subject represents an ApiData instance
204+
if (!$subject instanceof ApiData) {
205+
return;
206+
}
207+
208+
// only apply for specific api provider
209+
if( $subject->getApiProviderName() !== 'mailchimp') {
210+
return;
211+
}
212+
213+
// different fail scenarios can be applied:
214+
215+
$event->shouldFail('My invalid message for a specific channel! Allow further channels to pass!', true);
216+
217+
$event->shouldFail('My invalid message! If this happens, no further channel will be executed!', false);
218+
219+
// silently skip channel
220+
if ($subject->getProviderConfigurationNode('myConfig') === 'a special value') {
221+
$event->shouldSuspend();
222+
return;
223+
}
224+
}
225+
}
226+
```
227+
228+
**
229+
230+
## Example: Trigger Value
231+
In some scenarios, you want to trigger your API channel only, if a specific form value is given (A checkbox for example). We'll
232+
release this in a [dedicated feature](https://github.com/dachcom-digital/pimcore-formbuilder/issues/304) in upcoming versions.
233+
Until then, this can be solved by use a custom provider configuration field:
234+
235+
### Configuration Field
236+
First, we have to add a configuration field. Let's call it `consentTriggerValue`:
237+
```php
238+
// AppBundle\FormBuilder\ApiProvider\MailChimpApiProvider
239+
public function getProviderConfigurationFields(FormDefinitionInterface $formDefinition): array
240+
{
241+
return [
242+
[
243+
'type' => 'text',
244+
'label' => 'Consent Trigger Value',
245+
'name' => 'consentTriggerValue',
246+
'required' => false,
247+
],
248+
...
249+
];
250+
}
251+
```
252+
253+
### Trigger Field
254+
Then, we need to add a trigger field. Let's call it `CONSENT_TRIGGER`:
255+
```php
256+
// AppBundle\FormBuilder\ApiProvider\MailChimpApiProvider
257+
public function getPredefinedApiFields(FormDefinitionInterface $formDefinition, array $providerConfiguration): array
258+
{
259+
return [
260+
...
261+
'CONSENT_TRIGGER'
262+
];
263+
}
264+
```
265+
266+
### Channel Configuration
267+
Append our freshly created fields in the API output channel:
268+
![image](https://user-images.githubusercontent.com/700119/145799355-cc47cf5f-d5e5-464c-808c-1fc3abe30bec.png)
269+
![image](https://user-images.githubusercontent.com/700119/145799502-5a54d0e7-a7e3-401c-8a9f-bc20966894b6.png)
270+
271+
### Dispatch Process
272+
And finally, check given consent value in your `process` method:
273+
```php
274+
public function process(ApiData $apiData): void
275+
{
276+
$consentTriggerValue = $apiData->getProviderConfigurationNode('consentTriggerValue');
277+
278+
// configuration values are always available as strings
279+
if ($consentTriggerValue === 'true') {
280+
$consentTriggerValue = true;
281+
}
282+
283+
// in our example we're dealing with a checkbox.
284+
// which means that it's not available as node if unchecked
285+
if (!$apiData->hasApiNode('CONSENT_TRIGGER')) {
286+
return;
287+
}
288+
289+
// otherwise, it has to be the same value as defined in our configuration node!
290+
if ($consentTriggerValue !== $apiData->getApiNode('CONSENT_TRIGGER')) {
291+
return;
292+
}
293+
294+
...
295+
}
296+
```
297+
298+
And you're done. This API channel will only dispatch if a user gives consent to it.

docs/OutputWorkflow/0_Usage.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ As you can see, these are powerful tools which allows you to define workflows wi
3434
Want to learn more? Let's start with the [Email Channel](./10_EmailChannel.md).
3535

3636
## Output Workflow Topics
37+
- [API Channel](./09_ApiChannel.md)
3738
- [Email Channel](./10_EmailChannel.md)
3839
- [Object Channel](./11_ObjectChannel.md)
3940
- [Custom Channel](./12_CustomChannel.md)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Field Transformer
2+
3+
Field transformer can be used to transform fields by configuration.
4+
For example this comes in handy if you're using the [API Output Channel](./09_ApiChannel.md).
5+
6+
![image](https://user-images.githubusercontent.com/700119/146228508-f155c865-c0ef-4703-a409-7f59aaa59839.png)
7+
8+
## Service Registration
9+
10+
```yaml
11+
AppBundle\FormBuilder\FieldTransformer\PhoneNumberTransformer:
12+
autowire: true
13+
public: false
14+
tags:
15+
- { name: form_builder.output_workflow.field_transform, identifier: phoneNumberTransformer }
16+
17+
```
18+
19+
Within the service, you're able to modify your value.
20+
21+
```php
22+
<?php
23+
24+
namespace AppBundle\FormBuilder\FieldTransformer;
25+
26+
use FormBuilderBundle\OutputWorkflow\FieldTransformerInterface;
27+
use libphonenumber\PhoneNumberFormat;
28+
use libphonenumber\PhoneNumberUtil;
29+
30+
class PhoneNumberTransformer implements FieldTransformerInterface
31+
{
32+
public function getName(): string
33+
{
34+
return 'Phone Number Transformer';
35+
}
36+
37+
public function getDescription(): ?string
38+
{
39+
return 'Add your description here';
40+
}
41+
42+
public function transform($value, array $context): mixed
43+
{
44+
$phoneUtil = PhoneNumberUtil::getInstance();
45+
$phoneInstance = $phoneUtil->parse($value, 'DE');
46+
47+
return $phoneUtil->format($phoneInstance, PhoneNumberFormat::E164);
48+
}
49+
}
50+
```

0 commit comments

Comments
 (0)