feature/logging_Service (#17)

Reviewed-on: https://git.eckertplayground.de/taarly/PHP_AdminTool_Projekt/pulls/17
Co-authored-by: blaerf <blaerf@gmx.de>
Co-committed-by: blaerf <blaerf@gmx.de>
This commit is contained in:
blaerf 2025-12-05 07:42:10 +00:00 committed by blaerf
parent e0cd5591c5
commit 9a8d90a6db
5 changed files with 273 additions and 30 deletions

View File

@ -6,6 +6,8 @@ declare(strict_types=1);
namespace App\Controllers; namespace App\Controllers;
use App\Services\Ldap\LdapAuthService; use App\Services\Ldap\LdapAuthService;
use App\Services\Logging\LoggingService;
use Throwable;
/** /**
* Zuständig für alles rund um den Login: * Zuständig für alles rund um den Login:
@ -26,6 +28,9 @@ class AuthController
/** @var LdapAuthService Service, der die eigentliche LDAP/AD-Authentifizierung übernimmt */ /** @var LdapAuthService Service, der die eigentliche LDAP/AD-Authentifizierung übernimmt */
private LdapAuthService $ldapAuthService; private LdapAuthService $ldapAuthService;
/** @var LoggingService Logger für technische Fehler */
private LoggingService $logger;
/** /**
* Übergibt die Konfiguration an den Controller und initialisiert den LDAP-Authentifizierungsservice. * Übergibt die Konfiguration an den Controller und initialisiert den LDAP-Authentifizierungsservice.
* *
@ -39,6 +44,9 @@ class AuthController
// LdapAuthService mit dem Teilbereich "ldap" aus der Konfiguration initialisieren. // LdapAuthService mit dem Teilbereich "ldap" aus der Konfiguration initialisieren.
// Wenn 'ldap' nicht gesetzt ist, wird ein leeres Array übergeben (Fail fast erfolgt dann im Service). // Wenn 'ldap' nicht gesetzt ist, wird ein leeres Array übergeben (Fail fast erfolgt dann im Service).
$this->ldapAuthService = new LdapAuthService($config['ldap'] ?? []); $this->ldapAuthService = new LdapAuthService($config['ldap'] ?? []);
// LoggingService mit dem Teilbereich "logging" aus der Konfiguration initialisieren.
$this->logger = new LoggingService($config['logging'] ?? []);
} }
/** /**
@ -85,11 +93,26 @@ class AuthController
// true = Authentifizierung erfolgreich // true = Authentifizierung erfolgreich
// false = Anmeldedaten fachlich ungültig (Benutzer/Passwort falsch) // false = Anmeldedaten fachlich ungültig (Benutzer/Passwort falsch)
$authenticated = $this->ldapAuthService->authenticate($username, $password); $authenticated = $this->ldapAuthService->authenticate($username, $password);
} catch (\Throwable $exception) { } catch (Throwable $exception) {
// Technischer Fehler (z. B. LDAP-Server nicht erreichbar, falsche Konfiguration). // HIER ist vorher dein Fehler entstanden:
// In diesem Fall wird eine technische Fehlermeldung im Login-Formular angezeigt. // - showLoginForm() wurde nur aufgerufen, das Ergebnis aber ignoriert
// - danach kam ein "return;" ohne Rückgabewert → Rückgabetyp array wurde verletzt
// Technischen Fehler ausführlich ins Log schreiben
$this->logger->logException(
'Technischer Fehler bei der Anmeldung.',
$exception,
[
'route' => 'login.submit',
'username' => $username,
'remote_addr'=> $_SERVER['REMOTE_ADDR'] ?? null,
]
);
// Für den Benutzer nur eine allgemeine, aber verständliche Meldung anzeigen
return $this->showLoginForm( return $this->showLoginForm(
'Technischer Fehler bei der Anmeldung: ' . $exception->getMessage() 'Technischer Fehler bei der Anmeldung. Bitte versuchen Sie es später erneut '
. 'oder wenden Sie sich an den Administrator.'
); );
} }

View File

@ -6,6 +6,8 @@ declare(strict_types=1);
namespace App\Controllers; namespace App\Controllers;
use App\Services\Ldap\LdapDirectoryService; use App\Services\Ldap\LdapDirectoryService;
use App\Services\Logging\LoggingService;
use Throwable;
/** /**
* Controller für die Benutzer- und Gruppenanzeige. * Controller für die Benutzer- und Gruppenanzeige.
@ -30,6 +32,9 @@ class UserManagementController
/** @var LdapDirectoryService Service für das Lesen von Benutzern und Gruppen aus dem LDAP/AD */ /** @var LdapDirectoryService Service für das Lesen von Benutzern und Gruppen aus dem LDAP/AD */
private LdapDirectoryService $directoryService; private LdapDirectoryService $directoryService;
/** @var LoggingService Logger für technische Fehler */
private LoggingService $logger;
/** /**
* @param array<string, mixed> $config Vollständige Konfiguration aus config.php * @param array<string, mixed> $config Vollständige Konfiguration aus config.php
*/ */
@ -43,6 +48,9 @@ class UserManagementController
// Directory-Service initialisieren, der die eigentliche LDAP-Arbeit übernimmt. // Directory-Service initialisieren, der die eigentliche LDAP-Arbeit übernimmt.
$this->directoryService = new LdapDirectoryService($ldapConfig); $this->directoryService = new LdapDirectoryService($ldapConfig);
// Logging-Service initialisieren.
$this->logger = new LoggingService($config['logging'] ?? []);
} }
/** /**
@ -62,10 +70,19 @@ class UserManagementController
// Benutzer- und Gruppenlisten aus dem AD laden. // Benutzer- und Gruppenlisten aus dem AD laden.
$users = $this->directoryService->getUsers(); $users = $this->directoryService->getUsers();
$groups = $this->directoryService->getGroups(); $groups = $this->directoryService->getGroups();
} catch (\Throwable $exception) { } catch (Throwable $exception) {
// Sämtliche technischen Fehler (z. B. Verbindungs- oder Konfigurationsprobleme) // Technische Details ins Log, für den Benutzer eine allgemeine Meldung.
// werden hier in eine für den Benutzer lesbare Fehlermeldung übersetzt. $this->logger->logException(
$error = 'Fehler beim Laden von Benutzern/Gruppen: ' . $exception->getMessage(); 'Fehler beim Laden von Benutzern/Gruppen.',
$exception,
[
'route' => 'users',
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? null,
]
);
$error = 'Fehler beim Laden von Benutzern/Gruppen. '
. 'Bitte versuchen Sie es später erneut oder wenden Sie sich an den Administrator.';
} }
// Pfad zur eigentlichen View-Datei bestimmen. // Pfad zur eigentlichen View-Datei bestimmen.

View File

@ -0,0 +1,134 @@
<?php
// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
declare(strict_types=1);
namespace App\Services\Logging;
use DateTimeImmutable;
use Throwable;
/**
* Einfacher File-Logger für die AdminTool-Anwendung.
*
* Ziele:
* - Technische Details werden in eine Log-Datei unter public/logs/ geschrieben.
* - In der Weboberfläche erscheinen nur verständliche, fachliche Fehlermeldungen.
*/
class LoggingService
{
/** @var string Vollständiger Pfad zum Log-Verzeichnis */
private string $logDir;
/** @var string Dateiname der Log-Datei */
private string $logFile;
/** @var int Minimale Log-Stufe, ab der geschrieben wird. */
private int $minLevel;
/**
* Zuordnung der Log-Level zu numerischen Werten zur Filterung.
*
* @var array<string, int>
*/
private const array LEVEL_MAP = [
'debug' => 100,
'info' => 200,
'warning' => 300,
'error' => 400,
];
/**
* @param array<string, mixed> $config Teilkonfiguration "logging" aus config.php
*/
public function __construct(array $config)
{
// Standard: public/logs relativ zum Projektroot
$baseDir = $config['log_dir'] ?? (__DIR__ . '/../../../public/logs');
$fileName = $config['log_file'] ?? 'app.log';
$level = strtolower((string)($config['min_level'] ?? 'info'));
$this->logDir = rtrim($baseDir, DIRECTORY_SEPARATOR);
$this->logFile = $fileName;
$this->minLevel = self::LEVEL_MAP[$level] ?? self::LEVEL_MAP['info'];
$this->ensureLogDirectoryExists();
}
/**
* Stellt sicher, dass das Log-Verzeichnis existiert.
*/
private function ensureLogDirectoryExists(): void
{
if (is_dir($this->logDir) === true) {
return;
}
if (@mkdir($this->logDir, 0775, true) === false && is_dir($this->logDir) === false) {
// Wenn das Anlegen fehlschlägt, wenigstens einen Eintrag im PHP-Error-Log hinterlassen.
error_log(sprintf('LoggingService: Konnte Log-Verzeichnis "%s" nicht anlegen.', $this->logDir));
}
}
/**
* Allgemeiner Log-Eintrag.
*
* @param string $level Log-Level (debug|info|warning|error)
* @param string $message Nachrichtentext
* @param array<string, mixed> $context Zusätzliche Kontextinformationen
*/
public function log(string $level, string $message, array $context = []): void
{
$level = strtolower($level);
$numericLevel = self::LEVEL_MAP[$level] ?? self::LEVEL_MAP['error'];
// Alles unterhalb der minimalen Stufe ignorieren.
if ($numericLevel < $this->minLevel) {
return;
}
$timestamp = new DateTimeImmutable()->format('Y-m-d H:i:s');
$contextJson = $context === []
? '{}'
: (string)json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$line = sprintf(
"[%s] %-7s %s %s%s",
$timestamp,
strtoupper($level),
$message,
$contextJson,
PHP_EOL
);
$filePath = $this->logDir . DIRECTORY_SEPARATOR . $this->logFile;
if (@file_put_contents($filePath, $line, FILE_APPEND | LOCK_EX) === false) {
// Fallback, damit Fehler beim Logging selbst nicht die App zerschießen.
error_log(sprintf('LoggingService: Konnte in Log-Datei "%s" nicht schreiben.', $filePath));
}
}
/**
* Komfortmethode, um Exceptions strukturiert zu loggen.
*
* @param string $message Kurzer Kontexttext zur Exception
* @param Throwable $exception Die geworfene Exception
* @param array<string, mixed> $context Zusätzlicher Kontext (Route, Benutzername, Remote-IP, ...)
*/
public function logException(string $message, Throwable $exception, array $context = []): void
{
$exceptionContext = [
'exception_class' => get_class($exception),
'exception_message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
];
$mergedContext = array_merge($context, $exceptionContext);
$this->log('error', $message, $mergedContext);
}
}

View File

@ -49,4 +49,14 @@ return [
'storage_used' => '1.3.6.1.2.1.25.2.3.1.6', 'storage_used' => '1.3.6.1.2.1.25.2.3.1.6',
], ],
], ],
// Logging-Konfiguration
'logging' => [
// Standard: public/logs relativ zum Projekt-Root
'log_dir' => __DIR__ . '/../public/logs',
// Name der Logdatei
'log_file' => 'app.log',
// Minimale Stufe: debug, info, warning, error
'min_level' => 'info',
],
]; ];

View File

@ -23,6 +23,10 @@ declare(strict_types=1);
// Eine neue Session wird gestartet und die entsprechende Variable ($_SESSION) angelegt oder eine bestehende wird fortgesetzt. // Eine neue Session wird gestartet und die entsprechende Variable ($_SESSION) angelegt oder eine bestehende wird fortgesetzt.
session_start(); session_start();
// PHP-Fehler nur ins Log schreiben, nicht direkt im Browser anzeigen
error_reporting(E_ALL);
ini_set('display_errors', '0');
/* /*
* Registriert eine Autoload-Funktion für Klassen mit dem Namespace-Präfix "App\". * Registriert eine Autoload-Funktion für Klassen mit dem Namespace-Präfix "App\".
* Statt jede Klasse manuell über "require pfad_zur_klasse.php" einzubinden, * Statt jede Klasse manuell über "require pfad_zur_klasse.php" einzubinden,
@ -68,6 +72,61 @@ $config = require $configPath;
use App\Controllers\AuthController; use App\Controllers\AuthController;
use App\Controllers\DashboardController; use App\Controllers\DashboardController;
use App\Controllers\UserManagementController; use App\Controllers\UserManagementController;
use App\Services\Logging\LoggingService;
// Globalen Logger initialisieren, damit auch Fehler außerhalb der Controller
// (z. B. in index.php selbst) sauber protokolliert werden.
$globalLogger = new LoggingService($config['logging'] ?? []);
/**
* Globale Fehlerbehandlung:
* - PHP-Fehler (Warnings, Notices, ...) werden in den Logger geschrieben.
* - Unbehandelte Exceptions werden ebenfalls geloggt und führen zu einer generischen 500er-Meldung.
*/
set_error_handler(
static function (
int $severity,
string $message,
string $file = '',
int $line = 0
) use ($globalLogger): bool {
// Fehler nur loggen, wenn sie durch error_reporting() nicht unterdrückt sind.
if ((error_reporting() & $severity) === 0) {
return false;
}
$globalLogger->log(
'error',
'PHP-Fehler: ' . $message,
[
'severity' => $severity,
'file' => $file,
'line' => $line,
]
);
// false zurückgeben = PHP darf seinen Standard-Handler zusätzlich verwenden
// (der Browser sieht wegen display_errors=0 trotzdem nichts).
return false;
}
);
set_exception_handler(
static function (Throwable $exception) use ($globalLogger): void {
$globalLogger->logException(
'Unbehandelte Exception in der Anwendung.',
$exception,
[
'request_uri' => $_SERVER['REQUEST_URI'] ?? null,
'route' => $_GET['route'] ?? null,
]
);
http_response_code(500);
echo 'Es ist ein unerwarteter Fehler aufgetreten. '
. 'Bitte versuchen Sie es später erneut oder wenden Sie sich an den Administrator.';
}
);
/** /**
* Hilfsfunktion: Prüft, ob ein Benutzer eingeloggt ist. * Hilfsfunktion: Prüft, ob ein Benutzer eingeloggt ist.
@ -100,7 +159,7 @@ function handleResult(?array $result): void
} }
if (isset($result['redirect']) === true) { if (isset($result['redirect']) === true) {
header('Location: ' . (string)$result['redirect']); header('Location: ' . $result['redirect']);
exit; exit;
} }