| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- <?php
- declare(strict_types=1);
- namespace App\Form;
- final class Validator
- {
- /** @var array<string, array<string, mixed>> */
- private array $fields;
- public function __construct(FormSchema $schema)
- {
- $this->fields = $schema->getAllFields();
- }
- /**
- * @param array<string, mixed> $data
- * @param array<string, array<int, array<string, mixed>>> $uploads
- * @return array<string, string>
- */
- public function validateSubmit(array $data, array $uploads): array
- {
- $errors = [];
- foreach ($this->fields as $key => $field) {
- $type = $field['type'] ?? 'text';
- $visible = $this->isVisible($field, $data);
- if (!$visible) {
- continue;
- }
- $required = $this->isRequired($field, $data);
- if ($type === 'file') {
- $hasFile = !empty($uploads[$key]);
- if ($required && !$hasFile) {
- $errors[$key] = 'Dieses Upload-Feld ist erforderlich.';
- }
- continue;
- }
- $value = $data[$key] ?? null;
- if ($required && $this->isEmptyValue($value, $type, $field)) {
- $errors[$key] = 'Dieses Feld ist erforderlich.';
- continue;
- }
- if ($this->isEmptyValue($value, $type, $field)) {
- continue;
- }
- if ($type === 'email' && filter_var((string) $value, FILTER_VALIDATE_EMAIL) === false) {
- $errors[$key] = 'Bitte eine gültige E-Mail-Adresse eingeben.';
- }
- if ($type === 'select' && isset($field['options']) && is_array($field['options'])) {
- $selected = (string) $value;
- $validSelection = false;
- foreach ($field['options'] as $option) {
- if (!is_array($option)) {
- continue;
- }
- if ((string) ($option['value'] ?? '') !== $selected) {
- continue;
- }
- $validSelection = $this->isOptionVisible($option, $data);
- break;
- }
- if (!$validSelection) {
- $errors[$key] = 'Ungültige Auswahl.';
- }
- }
- if (isset($field['max_length']) && is_int($field['max_length'])) {
- if (strlen((string) $value) > $field['max_length']) {
- $errors[$key] = 'Eingabe ist zu lang.';
- }
- }
- }
- return $errors;
- }
- /** @param array<string, mixed> $field */
- private function isRequired(array $field, array $data): bool
- {
- if (!$this->isVisible($field, $data)) {
- return false;
- }
- if (($field['required'] ?? false) === true) {
- return true;
- }
- if (!isset($field['required_if']) || !is_array($field['required_if'])) {
- return false;
- }
- return $this->ruleMatches($field['required_if'], $data);
- }
- /** @param array<string, mixed> $field */
- private function isVisible(array $field, array $data): bool
- {
- if (!isset($field['visible_if']) || !is_array($field['visible_if'])) {
- return true;
- }
- return $this->ruleMatches($field['visible_if'], $data);
- }
- /** @param array<string, mixed> $rule */
- private function ruleMatches(array $rule, array $data): bool
- {
- $sourceField = (string) ($rule['field'] ?? '');
- $equals = (string) ($rule['equals'] ?? '');
- if ($sourceField === '') {
- return false;
- }
- return $this->resolveRuleValue($sourceField, $data) === $equals;
- }
- /** @param array<string, mixed> $data */
- private function resolveRuleValue(string $sourceField, array $data): string
- {
- if (array_key_exists($sourceField, $data)) {
- return (string) ($data[$sourceField] ?? '');
- }
- $derived = $this->derivedRuleValues($data);
- return (string) ($derived[$sourceField] ?? '');
- }
- /**
- * @param array<string, mixed> $data
- * @return array<string, string>
- */
- private function derivedRuleValues(array $data): array
- {
- $adultFlag = $this->deriveAdultFlag((string) ($data['geburtsdatum'] ?? ''));
- if ($adultFlag === null) {
- return [
- 'is_minor' => '',
- ];
- }
- return [
- 'is_minor' => $adultFlag ? '0' : '1',
- ];
- }
- private function deriveAdultFlag(string $birthdate): ?bool
- {
- $birthdate = trim($birthdate);
- if ($birthdate === '') {
- return null;
- }
- $date = \DateTimeImmutable::createFromFormat('!Y-m-d', $birthdate);
- if (!$date || $date->format('Y-m-d') !== $birthdate) {
- return null;
- }
- $today = new \DateTimeImmutable('today');
- if ($date > $today) {
- return null;
- }
- $age = $date->diff($today)->y;
- return $age >= 18;
- }
- /** @param array<string, mixed> $option */
- private function isOptionVisible(array $option, array $data): bool
- {
- if (isset($option['visible_if']) && is_array($option['visible_if']) && !$this->ruleMatches($option['visible_if'], $data)) {
- return false;
- }
- if (isset($option['hidden_if']) && is_array($option['hidden_if']) && $this->ruleMatches($option['hidden_if'], $data)) {
- return false;
- }
- return true;
- }
- /** @param array<string, mixed> $field */
- private function isEmptyValue(mixed $value, string $type, array $field): bool
- {
- if ($type === 'checkbox') {
- return !in_array((string) $value, ['1', 'on', 'true'], true);
- }
- if ($type === 'table') {
- return $this->isTableValueEmpty($value, $field);
- }
- return $value === null || trim((string) $value) === '';
- }
- /** @param array<string, mixed> $field */
- private function isTableValueEmpty(mixed $value, array $field): bool
- {
- $raw = trim((string) $value);
- if ($raw === '') {
- return true;
- }
- $lines = preg_split('/\R/', $raw);
- if (!is_array($lines)) {
- return true;
- }
- $rows = [];
- foreach ($lines as $line) {
- $line = trim((string) $line);
- if ($line === '') {
- continue;
- }
- $rows[] = str_getcsv($line);
- }
- if (empty($rows)) {
- return true;
- }
- $columnHeaders = [];
- if (isset($field['columns']) && is_array($field['columns'])) {
- foreach ($field['columns'] as $column) {
- if (!is_array($column)) {
- continue;
- }
- $label = trim((string) ($column['label'] ?? ''));
- if ($label !== '') {
- $columnHeaders[] = $label;
- }
- }
- }
- $dataRows = $rows;
- if (!empty($columnHeaders) && $this->tableRowMatchesHeader($rows[0], $columnHeaders)) {
- $dataRows = array_slice($rows, 1);
- }
- if (empty($dataRows)) {
- return true;
- }
- foreach ($dataRows as $dataRow) {
- foreach ((array) $dataRow as $cell) {
- if (trim((string) $cell) !== '') {
- return false;
- }
- }
- }
- return true;
- }
- /**
- * @param array<int, string> $row
- * @param array<int, string> $header
- */
- private function tableRowMatchesHeader(array $row, array $header): bool
- {
- if (count($row) !== count($header)) {
- return false;
- }
- foreach ($header as $index => $headerValue) {
- if (strtolower(trim((string) ($row[$index] ?? ''))) !== strtolower(trim($headerValue))) {
- return false;
- }
- }
- return true;
- }
- }
|