$definitions * @param array $values * @return array{values: array, errors: array, warnings: list} */ public function validate(array $definitions, array $values, bool $isCreate): array { $clean = []; $errors = []; $warnings = []; foreach ($values as $key => $value) { $key = (string) $key; $field = $definitions[$key] ?? null; if ($field === null || $field->status === FieldDefinition::HIDDEN) { $errors[$key] = 'Unbekanntes Feld; verfügbare Felder über lit/content-types (direkt: ' . 'lit-content-types) abrufen.'; continue; } if ($field->status === FieldDefinition::READONLY) { $errors[$key] = 'Feld ist schreibgeschützt.'; continue; } $error = $value === null ? null : $this->check($field, $value); if ($error !== null) { $errors[$key] = $error; continue; } if ($field->status === FieldDefinition::UNKNOWN) { $warnings[] = sprintf( 'Feld "%s" hat keinen bekannten Typ; der Wert wurde unverändert gespeichert.', $key ); } $clean[$key] = $value; } foreach ($definitions as $key => $field) { // A field the user cannot write (readonly, hidden) cannot be required from them either. $writable = in_array($field->status, [FieldDefinition::WRITABLE, FieldDefinition::UNKNOWN], true); if (! $field->required || ! $writable || isset($errors[$key])) { continue; } $provided = array_key_exists($key, $clean); if ($isCreate && (! $provided || $this->isEmpty($clean[$key]))) { $errors[$key] = 'Pflichtfeld fehlt.'; } elseif (! $isCreate && $provided && $this->isEmpty($clean[$key])) { $errors[$key] = 'Pflichtfeld darf nicht geleert werden.'; } } return ['values' => $clean, 'errors' => $errors, 'warnings' => $warnings]; } private function check(FieldDefinition $field, mixed $value): ?string { if ($field->multiple && in_array($field->type, self::LIST_TYPES, true)) { if (! is_array($value) || ! array_is_list($value)) { return 'Liste erwartet.'; } if ($field->type === 'array') { return null; } foreach ($value as $item) { $error = $this->checkScalar($field, $item); if ($error !== null) { return $error; } } return null; } return $this->checkScalar($field, $value); } private function checkScalar(FieldDefinition $field, mixed $value): ?string { switch ($field->type) { case 'string': case 'text': case 'password': return is_string($value) ? null : 'Text erwartet.'; case 'integer': return is_int($value) ? null : 'Ganzzahl erwartet.'; case 'number': return (is_int($value) || is_float($value)) ? null : 'Zahl erwartet.'; case 'boolean': return is_bool($value) ? null : 'true oder false erwartet.'; case 'date': return $this->matchesFormat($value, 'Y-m-d') ? null : 'Datum im Format JJJJ-MM-TT erwartet.'; case 'datetime': return $this->matchesFormat($value, 'Y-m-d H:i:s') ? null : 'Datum und Uhrzeit im Format JJJJ-MM-TT HH:MM:SS erwartet.'; case 'time': return $this->matchesFormat($value, 'H:i:s') ? null : 'Uhrzeit im Format HH:MM:SS erwartet.'; case 'choice': return $this->checkChoice($field, $value); case 'relation': case 'file': return $this->checkRelation($field, $value); case 'array': return (is_array($value) && array_is_list($value)) ? null : 'Liste erwartet.'; case 'object': return is_array($value) ? null : 'Objekt erwartet.'; default: return null; } } private function checkChoice(FieldDefinition $field, mixed $value): ?string { $allowed = array_column($field->choices ?? [], 'value'); return is_string($value) && in_array($value, $allowed, true) ? null : sprintf('Erlaubte Werte: %s.', implode(', ', $allowed)); } /** * The target must exist and be readable for the current user; an unreadable target gets * the same answer as a missing one and its actual post type is never named (no probing). */ private function checkRelation(FieldDefinition $field, mixed $value): ?string { if (! is_int($value) || $value <= 0) { return 'ID (positive Ganzzahl) erwartet.'; } $actual = get_post_type($value); $readable = $actual !== false && current_user_can('read_post', $value); $objectType = $field->related['object_type'] ?? ''; if ($objectType === 'media') { return $readable && $actual === 'attachment' ? null : sprintf('%d ist kein für dich lesbares Medium der Mediathek.', $value); } if ($objectType === 'post_type') { if (! $readable) { return sprintf('Eintrag %d existiert nicht oder ist für dich nicht lesbar.', $value); } $expected = $field->related['name'] ?? null; if ($expected !== null && $actual !== $expected) { return sprintf('Eintrag %d ist kein Eintrag vom Typ %s.', $value, $expected); } } return null; } private function matchesFormat(mixed $value, string $format): bool { if (! is_string($value)) { return false; } $parsed = DateTimeImmutable::createFromFormat('!' . $format, $value); return $parsed !== false && $parsed->format($format) === $value; } private function isEmpty(mixed $value): bool { return $value === null || $value === '' || $value === []; } }