This commit is contained in:
Martin Slachta
2026-06-11 19:03:29 +02:00
commit 0d829845c4
150 changed files with 38582 additions and 0 deletions
@@ -0,0 +1,20 @@
<?php
class RsvButtonElementHandler implements RsvFormElementHandler {
public function draw(RsvFormElementDefinition $element): void {
?>
<div class="rsv-form-input-group rsv-form-input-short">
<button class="rsv-form-btn-primary"><?= $element->getLabel() ?></button>
</div>
<?php
}
public function submit(RsvFormElementDefinition $def, int $submit_id, array $data, RsvFormSubmitResult $result): bool {
return true;
}
public function rollback(RsvFormElementDefinition $def, int $submit_id, array $data, RsvFormSubmitResult $result): void {
// No side effects to undo.
}
}
@@ -0,0 +1,21 @@
<?php
interface RsvFormElementHandler {
function draw(RsvFormElementDefinition $def) : void;
/**
* Validate and execute the element. Records errors on $result and returns
* false if it cannot complete.
*
* @param array<int,mixed> $data
*/
function submit(RsvFormElementDefinition $def, int $submit_id, array $data, RsvFormSubmitResult $result) : bool;
/**
* Undo a successful submit(); a no-op for elements without side effects.
*
* @param array<int,mixed> $data
*/
function rollback(RsvFormElementDefinition $def, int $submit_id, array $data, RsvFormSubmitResult $result) : void;
}
@@ -0,0 +1,87 @@
<?php
use Reservair\Logger\Logger;
class RsvFormReservationElementHandler implements RsvFormElementHandler {
private function end_from_start(DateTime $start, int $block_size_minutes): DateTime {
return ($start)->modify("+{$block_size_minutes} minutes");
}
public function draw(RsvFormElementDefinition $element): void {
$timetable_id = (int) $element->getAttr('timetable_id');
$price_per_block = (float) $element->getAttr('price_per_block', 0);
$name = $element->getName();
?>
<div class="rsv-form-input-group">
<label><?= esc_html($element->getLabel()) ?></label>
<rsv-reservation-selector
timetable-id="<?= $timetable_id ?>"
name="<?= esc_attr($name) ?>"
price-per-block="<?= $price_per_block ?>"
class="rsv-form-field"
></rsv-reservation-selector>
</div>
<?php
}
/** Validate the payload and create the reservation. */
public function submit(RsvFormElementDefinition $def, int $submit_id, array $data, RsvFormSubmitResult $result): bool {
$name = $def->getName();
$payload = $data[$name] ?? null;
if ($def->isRequired() && is_null($payload)) {
$result->addError($name, 'required', 'Reservation payload is required');
return false;
}
if (!is_array($payload) || !array_key_exists('timetable_reservations', $payload)) {
$result->addError($name, 'invalid', 'Reservation payload must be an object with timetable_reservations');
return false;
}
$timetable_id = intval($payload['timetable_id'] ?? null);
$timetable = (new RsvTimetableService())->get($timetable_id);
if (!$timetable) {
$result->addError($name, 'invalid', 'Invalid timetable ID');
return false;
}
$reservation = new RsvReservation(
0,
$submit_id,
false,
array_map(fn($t) => new RsvTimetableReservation(
0,
$timetable_id,
new DateTime($t),
$this->end_from_start(new DateTime($t), $timetable->block_size)
), $payload['timetable_reservations'])
);
try {
$reservation_id = (new RsvReservationService())->create($reservation);
} catch (\Throwable $e) {
// The slot may have been taken since the user selected it.
Logger::error($e);
$result->addError($name, 'creation_failed', 'Could not create the reservation. The slot may no longer be available.');
return false;
}
$result->setValue($name . '_reservation_id', $reservation_id);
$price_per_block = (float) $def->getAttr('price_per_block', 0);
$result->setValue($name . '_price', $price_per_block * count($payload['timetable_reservations']));
return true;
}
/** Delete the reservation created by submit(). */
public function rollback(RsvFormElementDefinition $def, int $submit_id, array $data, RsvFormSubmitResult $result): void {
$reservation_id = $result->getValue($def->getName() . '_reservation_id');
if ($reservation_id) {
(new RsvReservationService())->delete((int) $reservation_id);
}
}
}
@@ -0,0 +1,18 @@
<?php
class RsvReservationSummaryElementHandler implements RsvFormElementHandler {
public function draw(RsvFormElementDefinition $def): void {
?>
<rsv-reservation-summary></rsv-reservation-summary>
<?php
}
public function submit(RsvFormElementDefinition $def, int $submit_id, array $data, RsvFormSubmitResult $result): bool {
return true;
}
public function rollback(RsvFormElementDefinition $def, int $submit_id, array $data, RsvFormSubmitResult $result): void {
// No side effects to undo.
}
}
@@ -0,0 +1,82 @@
<?php
class RsvTextFieldElementHandler implements RsvFormElementHandler {
/** @return array<string, array{0: callable, 1: string}> */
private function rules(): array {
return [
'email' => ['is_email', 'Please enter a valid email address.'],
'phone' => [$this->regexPredicate('\+?[0-9 ()\-]{6,20}'), 'Please enter a valid phone number.'],
'digits' => [$this->regexPredicate('[0-9]+'), 'Please enter digits only.'],
];
}
private function regexPredicate(string $pattern): callable {
return fn($value): bool => (bool) @preg_match('~^(?:' . $pattern . ')$~u', (string) $value);
}
/** @return array{0:string,1:callable,2:string}|null */
private function resolveRule(RsvFormElementDefinition $def): ?array {
$name = $def->getAttr('validation', '');
if ($name === '') {
return null;
}
if ($name === 'pattern') {
$pattern = $def->getAttr('pattern', '');
if ($pattern === '') {
return null;
}
return ['pattern', $this->regexPredicate($pattern), $def->getAttr('pattern_message', '') ?: 'Invalid format.'];
}
$rules = $this->rules();
if (isset($rules[$name])) {
return [$name, $rules[$name][0], $rules[$name][1]];
}
return null;
}
public function draw(RsvFormElementDefinition $def): void {
$validation = $def->getAttr('validation', '');
$type = match ($validation) {
'email' => 'email',
'phone' => 'tel',
'digits' => 'number',
default => 'text',
};
$pattern = $validation === 'pattern' ? $def->getAttr('pattern', '') : '';
?>
<div class="rsv-form-input-group rsv-form-input-short">
<label class="rsv-form-label"><?= $def->getLabel() ?>:</label>
<input class="rsv-form-input rsv-form-field" type="<?= $type ?>" name="<?= $def->getName() ?>"<?= $pattern !== '' ? ' pattern="' . esc_attr($pattern) . '"' : '' ?> <?= $def->isRequired() ? "required" : "" ?>/>
<small class="rsv-form-small"><?= $def->getDesc() ?></small>
</div>
<?php
}
public function submit(RsvFormElementDefinition $def, int $submit_id, array $data, RsvFormSubmitResult $result): bool {
$name = $def->getName();
$value = $data[$name] ?? null;
if ($def->isRequired() && (is_null($value) || $value === "")) {
$result->addError($name, 'required', 'Field is required');
return false;
}
$rule = $this->resolveRule($def);
if ($rule !== null && !is_null($value) && $value !== "") {
[$code, $predicate, $message] = $rule;
if (!$predicate($value)) {
$result->addError($name, $code, $message);
return false;
}
}
$result->setValue($name, sanitize_text_field($value));
return true;
}
public function rollback(RsvFormElementDefinition $def, int $submit_id, array $data, RsvFormSubmitResult $result): void {
// No side effects to undo.
}
}