2026-06-14 14:13:37 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
use Reservair\Templating\RsvTemplateEngine;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validates a form definition before it is persisted.
|
|
|
|
|
*
|
|
|
|
|
* Template checks (symbols, syntax, custom elements) are delegated to the
|
|
|
|
|
* common template validator; on top of that this enforces form-level rules,
|
|
|
|
|
* such as requiring a submit button once the form defines any fields.
|
|
|
|
|
*/
|
|
|
|
|
final class RsvFormDefinitionValidator {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array<string,mixed> $definition The inner definition (elements, email_key, success_message).
|
|
|
|
|
* @return list<string> Human-readable problems; empty when the definition is valid.
|
|
|
|
|
*/
|
|
|
|
|
public function validate(array $definition): array {
|
|
|
|
|
$elements = is_array($definition['elements'] ?? null) ? $definition['elements'] : [];
|
|
|
|
|
$symbols = $this->symbols($elements);
|
|
|
|
|
$engine = $this->engine();
|
|
|
|
|
|
|
|
|
|
$errors = [];
|
|
|
|
|
|
|
|
|
|
// Templates reference submitted values by form-element name.
|
|
|
|
|
foreach ($this->templates($definition, $elements) as $label => $template) {
|
|
|
|
|
foreach ($engine->validate($template, $symbols) as $problem) {
|
|
|
|
|
$errors[] = "{$label}: {$problem}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A form that collects fields must give the visitor a way to send them.
|
|
|
|
|
if ($elements !== [] && !$this->has_submit($elements)) {
|
|
|
|
|
$errors[] = 'Form must contain a submit button.';
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 11:15:09 +02:00
|
|
|
// Validate membership bindings if present.
|
|
|
|
|
$errors = array_merge($errors, $this->validate_membership_bindings($definition));
|
|
|
|
|
|
2026-06-14 14:13:37 +02:00
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Names that templates may reference — the form's symbol table.
|
|
|
|
|
*
|
|
|
|
|
* @param array<int,mixed> $elements
|
|
|
|
|
* @return list<string>
|
|
|
|
|
*/
|
|
|
|
|
private function symbols(array $elements): array {
|
2026-06-16 19:33:55 +02:00
|
|
|
$names = RsvFormCalculatedValues::names(); // calculated values are referencable too
|
2026-06-14 14:13:37 +02:00
|
|
|
foreach ($elements as $el) {
|
|
|
|
|
$name = is_array($el) ? ($el['name'] ?? '') : '';
|
|
|
|
|
if (is_string($name) && $name !== '') {
|
|
|
|
|
$names[] = $name;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $names;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The definition's admin-authored templates, keyed by a label used to
|
|
|
|
|
* prefix any problems found in them.
|
|
|
|
|
*
|
|
|
|
|
* @param array<string,mixed> $definition
|
|
|
|
|
* @param array<int,mixed> $elements
|
|
|
|
|
* @return array<string,string>
|
|
|
|
|
*/
|
|
|
|
|
private function templates(array $definition, array $elements): array {
|
|
|
|
|
$templates = [];
|
|
|
|
|
|
|
|
|
|
$success = $definition['success_message'] ?? '';
|
|
|
|
|
if (is_string($success) && trim($success) !== '') {
|
|
|
|
|
$templates['Success message'] = $success;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($elements as $el) {
|
|
|
|
|
if (!is_array($el) || ($el['type'] ?? '') !== 'reservation') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$email_templates = $el['email_templates'] ?? [];
|
|
|
|
|
if (!is_array($email_templates)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
foreach (['on_accepted' => 'accepted', 'on_refused' => 'refused'] as $key => $human) {
|
|
|
|
|
$tpl = $email_templates[$key] ?? [];
|
|
|
|
|
if (!is_array($tpl)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
foreach (['subject', 'body'] as $part) {
|
|
|
|
|
$value = $tpl[$part] ?? '';
|
|
|
|
|
if (is_string($value) && trim($value) !== '') {
|
|
|
|
|
$templates["Email ({$human} {$part})"] = $value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $templates;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param array<int,mixed> $elements */
|
|
|
|
|
private function has_submit(array $elements): bool {
|
|
|
|
|
foreach ($elements as $el) {
|
|
|
|
|
if (is_array($el) && ($el['type'] ?? '') === 'button') {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function engine(): RsvTemplateEngine {
|
|
|
|
|
global $rsv_template_registry;
|
|
|
|
|
return new RsvTemplateEngine(registry: $rsv_template_registry);
|
|
|
|
|
}
|
2026-06-17 11:15:09 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array<string,mixed> $definition
|
|
|
|
|
* @return list<string>
|
|
|
|
|
*/
|
|
|
|
|
private function validate_membership_bindings(array $definition): array {
|
|
|
|
|
$errors = [];
|
|
|
|
|
$membership = $definition['membership'] ?? [];
|
|
|
|
|
|
|
|
|
|
if (!is_array($membership)) {
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$bindings = $membership['bindings'] ?? [];
|
|
|
|
|
if (!is_array($bindings)) {
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($bindings as $idx => $binding) {
|
|
|
|
|
if (!is_array($binding)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$program_id = intval($binding['program_id'] ?? 0);
|
|
|
|
|
$discount = floatval($binding['discount'] ?? 0.0);
|
|
|
|
|
|
|
|
|
|
if ($program_id <= 0) {
|
|
|
|
|
$errors[] = "Membership binding {$idx}: program_id must be a positive integer.";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($discount < 0.0 || $discount > 100.0) {
|
|
|
|
|
$errors[] = "Membership binding {$idx}: discount must be between 0 and 100.";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
2026-06-14 14:13:37 +02:00
|
|
|
}
|