togethere.cloud/public_html/api/DisciplineSettingsModel.php

423 lines
14 KiB
PHP

<?php
/**
* DisciplineSettingsModel.php
*
* Model dla ustawień dyscyplin (Ping-Pong, Papier-Kamień-Nożyce, Piłkarzyki)
* Obsługuje versioning, validację i trwałość danych
*/
class DisciplineSettingsModel
{
private $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
$this->ensureTableExists();
}
/**
* Upewnia się, że tabela settings_disciplines istnieje
*/
private function ensureTableExists()
{
$this->pdo->exec("
CREATE TABLE IF NOT EXISTS settings_disciplines (
id INT AUTO_INCREMENT PRIMARY KEY,
discipline VARCHAR(50) NOT NULL UNIQUE,
-- Reguły gry (logika)
pointsToWin INT NOT NULL DEFAULT 10,
setsToWin INT NOT NULL DEFAULT 2,
serveRotation INT NOT NULL DEFAULT 2,
specialRules TEXT,
-- Personalizacja UI (nie wpływa na logiką gry)
customization JSON,
-- Versioning ustawień
settingsVersion INT NOT NULL DEFAULT 1,
-- Metadane
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
updated_by INT,
INDEX idx_discipline (discipline),
INDEX idx_version (settingsVersion)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
");
}
/**
* Pobiera ustawienia dla dyscypliny
*
* @param string $discipline Nazwa dyscypliny (np. 'ping-pong')
* @return array|null Ustawienia lub null jeśli nie istnieją
*/
public function getSettings($discipline)
{
$stmt = $this->pdo->prepare("
SELECT * FROM settings_disciplines
WHERE discipline = :discipline
LIMIT 1
");
$stmt->execute([':discipline' => $discipline]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
// Rzutuj INT kolumny
$row['pointsToWin'] = (int)$row['pointsToWin'];
$row['setsToWin'] = (int)$row['setsToWin'];
$row['serveRotation'] = (int)$row['serveRotation'];
$row['settingsVersion'] = (int)$row['settingsVersion'];
$row['updated_by'] = $row['updated_by'] ? (int)$row['updated_by'] : null;
// Dekoduj JSON fields
if (!empty($row['customization'])) {
$row['customization'] = json_decode($row['customization'], true);
}
return $row;
}
/**
* Pobiera ustawienia z określonej wersji
* (do snapshot'ów przy starcie meczu)
*
* @param string $discipline Nazwa dyscypliny
* @param int $version Numer wersji
* @return array|null Ustawienia danej wersji
*/
public function getSettingsByVersion($discipline, $version)
{
// TODO: W przyszłości można dodać tabelę settings_disciplines_history
// dla pełnej historii zmian
$stmt = $this->pdo->prepare("
SELECT * FROM settings_disciplines
WHERE discipline = :discipline
AND settingsVersion = :version
LIMIT 1
");
$stmt->execute([
':discipline' => $discipline,
':version' => (int)$version
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
// Rzutuj INT kolumny
$row['pointsToWin'] = (int)$row['pointsToWin'];
$row['setsToWin'] = (int)$row['setsToWin'];
$row['serveRotation'] = (int)$row['serveRotation'];
$row['settingsVersion'] = (int)$row['settingsVersion'];
$row['updated_by'] = $row['updated_by'] ? (int)$row['updated_by'] : null;
if (!empty($row['customization'])) {
$row['customization'] = json_decode($row['customization'], true);
}
return $row;
}
/**
* Aktualizuje ustawienia dla dyscypliny
* Automatycznie zwiększa versioning
*
* @param string $discipline Nazwa dyscypliny
* @param array $settings Nowe ustawienia
* @param int $userId ID użytkownika wykonującego zmianę
* @return array Zaktualizowane ustawienia
* @throws Exception
*/
public function updateSettings($discipline, array $settings, $userId)
{
// Waliduj dane
$this->validateSettingsInput($settings);
// Pobierz obecne ustawienia, aby zwiększyć versioning
$current = $this->getSettings($discipline);
$newVersion = ($current ? (int)$current['settingsVersion'] + 1 : 1);
// Przygotuj dane do insertu/update
$data = [
':discipline' => $discipline,
':pointsToWin' => (int)$settings['pointsToWin'],
':setsToWin' => (int)$settings['setsToWin'],
':serveRotation' => (int)$settings['serveRotation'],
':specialRules' => $settings['specialRules'] ?? null,
':customization' => !empty($settings['customization'])
? json_encode($settings['customization'], JSON_UNESCAPED_UNICODE)
: null,
':settingsVersion' => $newVersion,
':updated_by' => $userId
];
$this->pdo->beginTransaction();
try {
if ($current) {
// UPDATE
$stmt = $this->pdo->prepare("
UPDATE settings_disciplines SET
pointsToWin = :pointsToWin,
setsToWin = :setsToWin,
serveRotation = :serveRotation,
specialRules = :specialRules,
customization = :customization,
settingsVersion = :settingsVersion,
updated_by = :updated_by,
updated_at = NOW()
WHERE discipline = :discipline
");
} else {
// INSERT (nowa dyscyplina)
$stmt = $this->pdo->prepare("
INSERT INTO settings_disciplines (
discipline,
pointsToWin,
setsToWin,
serveRotation,
specialRules,
customization,
settingsVersion,
updated_by
) VALUES (
:discipline,
:pointsToWin,
:setsToWin,
:serveRotation,
:specialRules,
:customization,
:settingsVersion,
:updated_by
)
");
}
$stmt->execute($data);
$result = $this->getSettings($discipline);
$this->pdo->commit();
return $result;
} catch (Exception $e) {
$this->pdo->rollBack();
throw $e;
}
}
/**
* Waliduje dane wejściowe ustawień
*
* @param array $settings Ustawienia do walidacji
* @throws InvalidArgumentException
*/
private function validateSettingsInput(array $settings)
{
$errors = [];
// Walidacja pointsToWin
if (!isset($settings['pointsToWin'])) {
$errors[] = 'pointsToWin is required';
} else {
$ptw = (int)$settings['pointsToWin'];
if ($ptw < 1 || $ptw > 100) {
$errors[] = 'pointsToWin must be between 1 and 100';
}
}
// Walidacja setsToWin
if (!isset($settings['setsToWin'])) {
$errors[] = 'setsToWin is required';
} else {
$stw = (int)$settings['setsToWin'];
if ($stw < 1 || $stw > 100) {
$errors[] = 'setsToWin must be between 1 and 100';
}
}
// Walidacja serveRotation
if (!isset($settings['serveRotation'])) {
$errors[] = 'serveRotation is required';
} else {
$sr = (int)$settings['serveRotation'];
if ($sr < 1 || $sr > 50) {
$errors[] = 'serveRotation must be between 1 and 50';
}
}
// Walidacja specialRules (opcjonalne, ale jeśli podane to string)
if (isset($settings['specialRules']) && !is_string($settings['specialRules'])) {
$errors[] = 'specialRules must be a string';
}
// Walidacja customization (opcjonalne, ale jeśli podane to musi być array/object)
if (isset($settings['customization'])) {
if (!is_array($settings['customization']) && !is_object($settings['customization'])) {
$errors[] = 'customization must be an object/array';
}
}
// Logika biznesowa
// Remis - wymuszenie override reguł
if (isset($settings['pointsToWin']) && isset($settings['setsToWin'])) {
$ptw = (int)$settings['pointsToWin'];
$stw = (int)$settings['setsToWin'];
// Jeśli oba są parzyste, możliwy jest remis - lepiej wymusić nieparzyste
// W przypadku remisu w ostatnim secie, gracze muszą grać dalej
if ($ptw % 2 === 0 || $stw % 2 === 0) {
$errors[] = 'pointsToWin and setsToWin should be odd numbers to avoid draws in final set';
}
}
if (!empty($errors)) {
throw new InvalidArgumentException(implode('; ', $errors));
}
}
/**
* Zwraca domyślne ustawienia dla dyscypliny
*
* @param string $discipline Nazwa dyscypliny
* @return array Domyślne ustawienia
*/
public static function getDefaults($discipline = 'ping-pong')
{
$defaults = [
'ping-pong' => [
'pointsToWin' => 11,
'setsToWin' => 3,
'serveRotation' => 2,
'specialRules' => 'Deuce at 10-10 (play until 2 points ahead)',
'customization' => [
'tableColor' => '#2d5016',
'ballColor' => '#ff6600',
'paddleColor' => '#000000',
'uiTheme' => 'dark'
]
],
'rock-paper-scissors' => [
'pointsToWin' => 5,
'setsToWin' => 1,
'serveRotation' => 1,
'specialRules' => 'Best of 1, instant rounds',
'customization' => [
'animationSpeed' => 'fast',
'uiTheme' => 'light'
]
],
'table-football' => [
'pointsToWin' => 5,
'setsToWin' => 1,
'serveRotation' => 3,
'specialRules' => 'Standard foosball rules, auto-restart after goal',
'customization' => [
'tableColor' => '#000000',
'figureColor' => '#ffffff',
'uiTheme' => 'dark'
]
]
];
return $defaults[$discipline] ?? $defaults['ping-pong'];
}
/**
* Inicjalizuje ustawienia dla dyscypliny (jeśli nie istnieją)
*
* @param string $discipline Nazwa dyscypliny
* @param int $userId ID administratora
*/
public function initializeIfNotExists($discipline, $userId)
{
if (!$this->getSettings($discipline)) {
$defaults = self::getDefaults($discipline);
$this->updateSettings($discipline, $defaults, $userId);
}
}
/**
* Pobiera snapshot ustawień dla meczu
* (snapshot to kopia ustawień w momencie startu meczu)
*
* @param string $discipline Nazwa dyscypliny
* @param int $version Opcjonalnie: wersja ustawień. Jeśli null, bierze najnowsze.
* @return array Snapshot do zapisania w meczu
*/
public function getSnapshot($discipline, $version = null)
{
if ($version !== null) {
$settings = $this->getSettingsByVersion($discipline, (int)$version);
} else {
$settings = $this->getSettings($discipline);
}
if (!$settings) {
throw new RuntimeException("Settings not found for discipline: $discipline");
}
return [
'discipline' => $discipline,
'settingsVersion' => (int)$settings['settingsVersion'],
'rules' => [
'pointsToWin' => (int)$settings['pointsToWin'],
'setsToWin' => (int)$settings['setsToWin'],
'serveRotation' => (int)$settings['serveRotation'],
'specialRules' => $settings['specialRules']
],
'snapshotTimestamp' => $settings['updated_at']
];
}
/**
* Pobiera historię zmian dla dyscypliny
* (przydatne do debuggowania i audytu)
*
* @param string $discipline Nazwa dyscypliny
* @return array Historia
*/
public function getHistory($discipline)
{
// TODO: W przyszłości należy dodać tabelę settings_disciplines_history
// Po prostu zwracamy obecne dane z metadata
$current = $this->getSettings($discipline);
if (!$current) {
return [];
}
return [
[
'version' => $current['settingsVersion'],
'updated_at' => $current['updated_at'],
'updated_by' => $current['updated_by'],
'changes' => 'Latest version'
]
];
}
/**
* Czyści ustawienia dyscypliny z bazy (dla testów)
*
* @param string $discipline Nazwa dyscypliny do usunięcia
* @return bool True jeśli usunięto
*/
public function deleteSettings($discipline)
{
try {
$stmt = $this->pdo->prepare("DELETE FROM settings_disciplines WHERE discipline = ?");
return $stmt->execute([$discipline]);
} catch (Exception $e) {
return false;
}
}
}
?>