Formular-Infrastruktur: Contact/Demo/Helpdesk mit E-Mail- und Salesforce-Delivery, JSON-Renderer-Erweiterungen, Crop-Variants
Build-Artefakte in public/_frontend/assets nicht enthalten.
This commit is contained in:
1 parent
eae40f4286
commit
a739dd8fe3
51 files changed
+1598
-303
No files matched your search
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Forms\Delivery;
|
||||
|
||||
/**
|
||||
* Delivery strategy for VITEC form submissions.
|
||||
* Implementations MUST throw on failure — the middleware logs the error
|
||||
* on the stored submission and keeps the data safe.
|
||||
*/
|
||||
interface DeliveryInterface
|
||||
{
|
||||
/**
|
||||
* @param array<string,string> $payload validated, filtered field values
|
||||
* @param array<string,mixed> $settings FlexForm settings of the form plugin
|
||||
*/
|
||||
public function deliver(string $formKey, array $payload, array $settings): void;
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Forms\Delivery;
|
||||
|
||||
use TYPO3\CMS\Core\Mail\MailMessage;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Sends the submission as a plain-text mail to the FlexForm-configured
|
||||
* recipient(s). Sender = system default (MAIL settings); reply-to = the
|
||||
* submitter's email when present.
|
||||
*/
|
||||
final class EmailDelivery implements DeliveryInterface
|
||||
{
|
||||
public function deliver(string $formKey, array $payload, array $settings): void
|
||||
{
|
||||
$recipients = GeneralUtility::trimExplode(',', (string)($settings['recipient'] ?? ''), true);
|
||||
if ($recipients === []) {
|
||||
throw new \RuntimeException('No recipient configured in the form plugin (FlexForm "Recipient").');
|
||||
}
|
||||
|
||||
$subject = trim((string)($settings['subject'] ?? ''));
|
||||
if ($subject === '') {
|
||||
$subject = 'VITEC website form: ' . $formKey;
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
foreach ($payload as $field => $value) {
|
||||
$lines[] = str_pad($field . ':', 16) . $value;
|
||||
}
|
||||
|
||||
$mail = GeneralUtility::makeInstance(MailMessage::class);
|
||||
$mail->to(...$recipients)
|
||||
->subject($subject)
|
||||
->text("Form: $formKey\n\n" . implode("\n", $lines));
|
||||
|
||||
$submitterEmail = trim((string)($payload['email'] ?? ''));
|
||||
if ($submitterEmail !== '' && filter_var($submitterEmail, FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->replyTo($submitterEmail);
|
||||
}
|
||||
|
||||
if (!$mail->send()) {
|
||||
throw new \RuntimeException('MailMessage::send() reported failure.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Forms\Delivery;
|
||||
|
||||
/**
|
||||
* Salesforce delivery — PREPARED STUB.
|
||||
*
|
||||
* The old website posted the contact form to Salesforce Web-to-Lead
|
||||
* (custom field ids like 00N3z00000DCiyV). When credentials / the target
|
||||
* setup are decided, implement deliver() here (Web-to-Lead POST or REST
|
||||
* API) including the camelCase -> Salesforce field mapping. Until then any
|
||||
* submission with delivery=salesforce is stored with status "failed" and
|
||||
* this message — no data is lost.
|
||||
*/
|
||||
final class SalesforceDelivery implements DeliveryInterface
|
||||
{
|
||||
public function deliver(string $formKey, array $payload, array $settings): void
|
||||
{
|
||||
throw new \RuntimeException(
|
||||
'Salesforce delivery is prepared but not configured yet — submission kept in the log.'
|
||||
);
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Forms;
|
||||
|
||||
/**
|
||||
* Single source of truth for the VITEC form plugins.
|
||||
*
|
||||
* The SAME definition drives (a) the JSON the React frontend renders the
|
||||
* form from and (b) the server-side validation of submissions in
|
||||
* FormSubmissionMiddleware. Field names are camelCase (React contract);
|
||||
* mapping to Salesforce field ids happens inside the Salesforce delivery.
|
||||
*
|
||||
* Field spec: name, type (text|email|tel|select|textarea), label, required,
|
||||
* options (select only) OR optionsSource (frontend-supplied list, e.g. the
|
||||
* ISO country list).
|
||||
*/
|
||||
final class FormDefinitions
|
||||
{
|
||||
public const HONEYPOT_FIELD = '_website';
|
||||
|
||||
/** CType => form key */
|
||||
public const CTYPE_MAP = [
|
||||
'vitec_contactform' => 'contact',
|
||||
'vitec_demoform' => 'demo',
|
||||
'vitec_helpdeskform' => 'helpdesk',
|
||||
];
|
||||
|
||||
private const SOLUTION_OPTIONS = [
|
||||
'IPTV Distribution',
|
||||
'Digital Signage',
|
||||
'Video Streaming',
|
||||
'Video Contribution',
|
||||
'Situational Awareness and ISR',
|
||||
'Remote Production',
|
||||
'Encoders & Decoders',
|
||||
'Custom Design Service',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public static function get(string $formKey): ?array
|
||||
{
|
||||
$all = self::all();
|
||||
return $all[$formKey] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array<string,mixed>>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
$contactFields = [
|
||||
self::text('firstName', 'First Name', true),
|
||||
self::text('lastName', 'Last Name', true),
|
||||
self::text('company', 'Company', true),
|
||||
self::field('email', 'email', 'Email', true),
|
||||
self::field('phone', 'tel', 'Phone', true),
|
||||
self::text('street', 'Street', false),
|
||||
self::text('city', 'City', false),
|
||||
self::text('zip', 'Zip / Postal Code', false),
|
||||
self::countrySelect(),
|
||||
self::select('state', 'State', false, [], 'usStates'),
|
||||
self::select('solution', 'Solution of Interest', false, self::SOLUTION_OPTIONS),
|
||||
self::field('message', 'textarea', 'Message', false),
|
||||
];
|
||||
|
||||
return [
|
||||
'contact' => [
|
||||
'formKey' => 'contact',
|
||||
'title' => 'Contact VITEC',
|
||||
'fields' => $contactFields,
|
||||
],
|
||||
'demo' => [
|
||||
'formKey' => 'demo',
|
||||
'title' => 'Request a Demo',
|
||||
'fields' => $contactFields,
|
||||
],
|
||||
'helpdesk' => [
|
||||
'formKey' => 'helpdesk',
|
||||
'title' => 'Request Access to Online HelpDesk',
|
||||
'fields' => [
|
||||
self::text('firstName', 'First Name', true),
|
||||
self::text('lastName', 'Last Name', true),
|
||||
self::text('company', 'Company', true),
|
||||
self::field('phone', 'tel', 'Phone Number', false),
|
||||
self::field('email', 'email', 'Email Address', true),
|
||||
self::countrySelect(true),
|
||||
self::text('product', 'Product', false),
|
||||
self::text('serialNumber', 'Serial Number', false),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a submission payload against the definition.
|
||||
*
|
||||
* @param array<string,mixed> $payload
|
||||
* @return array<string,string> field => error (empty = valid)
|
||||
*/
|
||||
public static function validate(string $formKey, array $payload): array
|
||||
{
|
||||
$definition = self::get($formKey);
|
||||
if ($definition === null) {
|
||||
return ['_form' => 'Unknown form'];
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
foreach ($definition['fields'] as $field) {
|
||||
$name = $field['name'];
|
||||
$value = trim((string)($payload[$name] ?? ''));
|
||||
|
||||
if (($field['required'] ?? false) && $value === '') {
|
||||
$errors[$name] = 'This field is required.';
|
||||
continue;
|
||||
}
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
if ($field['type'] === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[$name] = 'Please enter a valid email address.';
|
||||
}
|
||||
if (mb_strlen($value) > 5000) {
|
||||
$errors[$name] = 'Value is too long.';
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce a raw payload to the defined fields (drops everything unknown).
|
||||
*
|
||||
* @param array<string,mixed> $payload
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public static function filterPayload(string $formKey, array $payload): array
|
||||
{
|
||||
$definition = self::get($formKey);
|
||||
if ($definition === null) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($definition['fields'] as $field) {
|
||||
$out[$field['name']] = trim((string)($payload[$field['name']] ?? ''));
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ field DSL
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function field(string $name, string $type, string $label, bool $required): array
|
||||
{
|
||||
return ['name' => $name, 'type' => $type, 'label' => $label, 'required' => $required];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function text(string $name, string $label, bool $required): array
|
||||
{
|
||||
return self::field($name, 'text', $label, $required);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $options
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function select(string $name, string $label, bool $required, array $options, string $optionsSource = ''): array
|
||||
{
|
||||
$field = self::field($name, 'select', $label, $required);
|
||||
if ($options !== []) {
|
||||
$field['options'] = $options;
|
||||
}
|
||||
if ($optionsSource !== '') {
|
||||
// The frontend supplies this list (e.g. ISO countries) — keeps
|
||||
// the definition lean and the lists consistent app-wide.
|
||||
$field['optionsSource'] = $optionsSource;
|
||||
}
|
||||
return $field;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function countrySelect(bool $required = false): array
|
||||
{
|
||||
return self::select('country', 'Country', $required, [], 'countries');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user