Files
VITEC-website/packages/vitec/Classes/Forms/FormDefinitions.php

191 lines
6.2 KiB
PHP
Executable File

<?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');
}
}