216 lines
8.4 KiB
PHP
216 lines
8.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
final class EntryRepository
|
|
{
|
|
private EntryCrypto $crypto;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->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 = [
|
|
'<!-- mood-tracker:v2 -->',
|
|
'# 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);
|
|
}
|
|
}
|