crypto = new EntryCrypto(); } public function save(string $username, string $date, array $entry, array $evaluation): void { $path = $this->pathFor($username, $date); $directory = dirname($path); if (!is_dir($directory)) { mkdir($directory, 0775, true); } $markdown = $this->toMarkdown($username, $date, $entry, $evaluation); file_put_contents($path, $this->crypto->encrypt($markdown), LOCK_EX); } public function find(string $username, string $date): ?array { $path = $this->pathFor($username, $date); if (!is_file($path)) { return null; } $content = (string) file_get_contents($path); $plaintext = $this->crypto->decrypt($content); if ($this->crypto->shouldEncrypt() && !$this->crypto->isEncrypted($content)) { file_put_contents($path, $this->crypto->encrypt($plaintext), LOCK_EX); } return $this->parse($plaintext, $date); } public function all(string $username): array { $directory = $this->directoryFor($username); if (!is_dir($directory)) { return []; } $files = glob($directory . '/*.txt') ?: []; rsort($files, SORT_STRING); $entries = []; foreach ($files as $file) { $date = basename($file, '.txt'); $content = (string) file_get_contents($file); $plaintext = $this->crypto->decrypt($content); if ($this->crypto->shouldEncrypt() && !$this->crypto->isEncrypted($content)) { file_put_contents($file, $this->crypto->encrypt($plaintext), LOCK_EX); } $parsed = $this->parse($plaintext, $date); if ($parsed !== null) { $entries[] = $parsed; } } return $entries; } public function parseMarkdown(string $content, string $fallbackDate): ?array { $plaintext = $this->crypto->decrypt($content); return $this->parse($plaintext, $fallbackDate); } public function exportMarkdown(string $username, string $date, array $entry, array $evaluation): string { return $this->toMarkdown($username, $date, $entry, $evaluation); } private function directoryFor(string $username): string { return storage_path('users/' . normalize_username($username) . '/days'); } private function pathFor(string $username, string $date): string { return $this->directoryFor($username) . '/' . $date . '.txt'; } private function parse(string $content, string $fallbackDate): ?array { $sportTypes = []; $sportTypesRaw = (string) ($this->extract('/^- Sportarten:\s*(.*)$/mu', $content) ?? ''); if ($sportTypesRaw !== '') { preg_match_all('/\[([^\]]+)\]/u', $sportTypesRaw, $matches); $sportTypes = normalize_sport_type_selection($matches[1] ?? []); } if ($sportTypes === []) { $sportType = (string) ($this->extract('/^- Sportart:\s*(.*)$/mu', $content) ?? ''); if (preg_match('/\[(.+)\]\s*$/u', $sportType, $matches)) { $sportType = trim((string) ($matches[1] ?? '')); } $sportTypes = normalize_sport_type_selection($sportType); } $walkModeRaw = strtolower((string) ($this->extract('/^- Spaziergang-Modus:\s*(.+)$/mu', $content) ?? 'zeit')); $walkMode = $walkModeRaw === 'schritte' ? 'steps' : 'time'; $walkValue = (int) ($this->extract('/^- Spaziergang:\s*(.+)$/m', $content) ?? 0); $painRaw = $this->extract('/^- Schmerzen:\s*(.+)$/mu', $content); $alcoholRaw = strtolower((string) ($this->extract('/^- Alkohol:\s*(.+)$/mu', $content) ?? 'nein')); $entry = [ 'date' => $this->extract('/^Datum:\s*(\d{4}-\d{2}-\d{2})$/m', $content) ?? $fallbackDate, 'mood' => (int) ($this->extract('/^- Stimmung:\s*(.+)$/m', $content) ?? 5), 'energy' => (int) ($this->extract('/^- Energie:\s*(.+)$/m', $content) ?? 5), 'stress' => (int) ($this->extract('/^- Stress:\s*(.+)$/m', $content) ?? 5), 'pain' => $painRaw !== null ? (int) $painRaw : 1, 'pain_enabled' => $painRaw !== null, 'sleep_hours' => (float) ($this->extract('/^- Schlafdauer:\s*(.+)$/m', $content) ?? 0), 'sleep_feeling' => (int) ($this->extract('/^- Schlaf(?:gefühl|gefuehl):\s*(.+)$/mu', $content) ?? 3), 'sport_minutes' => (int) ($this->extract('/^- Sport:\s*(.+)$/m', $content) ?? 0), 'sport_type' => $sportTypes[0] ?? '', 'sport_types' => $sportTypes, 'walk_mode' => $walkMode, 'walk_minutes' => $walkMode === 'time' ? $walkValue : 0, 'walk_steps' => $walkMode === 'steps' ? $walkValue : 0, 'alcohol' => in_array($alcoholRaw, ['ja', 'yes', 'true', '1'], true), 'note' => $this->extractNote($content), ]; return $entry; } private function extract(?string $pattern, string $content): ?string { if ($pattern === null) { return null; } if (!preg_match($pattern, $content, $matches)) { return null; } return trim((string) ($matches[1] ?? '')); } private function extractNote(string $content): string { if (!preg_match('/^## Notiz\s*$\R?([\s\S]*)\z/m', $content, $matches)) { return ''; } return trim((string) ($matches[1] ?? '')); } private function toMarkdown(string $username, string $date, array $entry, array $evaluation): string { $sportTypes = $evaluation['sport_types'] ?? []; $sportTypeValues = array_map( static fn (array $type): string => (string) $type['label'] . ' [' . (string) $type['id'] . ']', array_filter($sportTypes, 'is_array') ); $lines = [ '', '# Stimmungstracker', 'Datum: ' . $date, 'Benutzer: ' . normalize_username($username), '', '## Werte', '- Stimmung: ' . $entry['mood'], '- Energie: ' . $entry['energy'], '- Stress: ' . $entry['stress'], ...(!empty($entry['pain_enabled']) ? ['- Schmerzen: ' . $entry['pain']] : []), '- Schlafdauer: ' . $entry['sleep_hours'], '- Schlafgefühl: ' . $entry['sleep_feeling'], '- Sport: ' . $entry['sport_minutes'], '- Sportarten: ' . implode(', ', $sportTypeValues), '- Spaziergang-Modus: ' . (($entry['walk_mode'] ?? 'time') === 'steps' ? 'schritte' : 'zeit'), '- Spaziergang: ' . (($entry['walk_mode'] ?? 'time') === 'steps' ? (int) ($entry['walk_steps'] ?? 0) : (int) ($entry['walk_minutes'] ?? 0)), '- Alkohol: ' . (!empty($entry['alcohol']) ? 'ja' : 'nein'), '', '## Bewertung', '- Punkte: ' . format_points((float) $evaluation['total']) . ' / ' . format_points((float) $evaluation['max_total']), '- Urteil: ' . $evaluation['label'], '', '## Punktedetails', '- Stimmung: ' . format_points((float) $evaluation['components']['mood']), '- Energie: ' . format_points((float) $evaluation['components']['energy']), '- Stress: ' . format_points((float) $evaluation['components']['stress']), ...(array_key_exists('pain', $evaluation['components']) ? ['- Schmerzen: ' . format_points((float) $evaluation['components']['pain'])] : []), '- Schlafdauer: ' . format_points((float) $evaluation['components']['sleep_hours']), '- Schlafgefühl: ' . format_points((float) $evaluation['components']['sleep_feeling']), '- Sport: ' . format_points((float) $evaluation['components']['sport_minutes']), '- Sportbonus: ' . format_points((float) ($evaluation['components']['sport_bonus'] ?? 0)), '- Spaziergang: ' . format_points((float) $evaluation['components']['walk_minutes']), '- Alkohol: ' . format_points((float) ($evaluation['components']['alcohol'] ?? 0)), '- Notiz: ' . format_points((float) $evaluation['components']['note']), '', '## Notiz', trim((string) $entry['note']), '', ]; return implode("\n", $lines); } }