abschluss

This commit is contained in:
2025-06-20 07:55:37 +00:00
parent 497e6a0bdf
commit 6c2e71dd53
34 changed files with 1370 additions and 382 deletions

39
app/Core/Container.php Normal file
View File

@ -0,0 +1,39 @@
<?php
namespace Blog\Core;
use Exception;
/**
* Ein einfacher Dependency Injection Container.
*/
class Container {
/**
* @var array Liste der registrierten Instanzen.
*/
private array $instances = [];
/**
* Registriert eine Instanz oder Factory-Funktion im Container.
*
* @param string $key Der eindeutige Schlüssel für die Instanz.
* @param callable $factory Eine Factory-Funktion, die eine Instanz erzeugt.
* @return void
*/
public function set(string $key, callable $factory): void {
$this->instances[$key] = $factory;
}
/**
* Ruft eine registrierte Instanz ab und erstellt sie falls nötig.
*
* @param string $key Der Schlüssel der angeforderten Instanz.
* @return mixed Die abgerufene Instanz.
* @throws Exception Wenn keine Instanz mit dem gegebenen Schlüssel existiert.
*/
public function get(string $key): mixed {
if(!isset($this->instances[$key])) {
throw new Exception("No instance found for {$key}");
}
return $this->instances[$key]($this);
}
}

View File

@ -1,18 +1,40 @@
<?php
namespace Blog\Core;
use Blog\Middleware\middlewareInterface;
use Blog\Http\request;
use Blog\Http\response;
use Blog\Middleware\MiddlewareInterface;
use Blog\Http\Request;
use Blog\Http\Response;
/**
* Router-Klasse für das Routing von HTTP-Anfragen.
*/
class Router {
/**
* @var array Liste der registrierten Routen.
*/
private array $routes = [];
/**
* Fügt eine neue Route hinzu.
*
* @param string $method HTTP-Methode (z.B. GET, POST).
* @param string $path Pfad der Route, kann Platzhalter enthalten.
* @param callable $handler Handler-Funktion für die Route.
* @param array $middlewares Liste der Middlewares für die Route.
* @return void
*/
public function addRoute(string $method, string $path, callable $handler, array $middlewares = []): void {
$path = preg_replace('/{(\w+)}/', '(?P<$1>[^/]+)', $path);
$this->routes[] = compact("method", "path", "handler", "middlewares");
}
/**
* Verarbeitet eine eingehende HTTP-Anfrage und sucht nach einer passenden Route.
*
* @param Request $req Die eingehende HTTP-Anfrage.
* @param Response $res Die HTTP-Antwort.
* @return void
*/
public function dispatch(Request $req, Response $res): void {
$method = $req->getMethod();
$uri = $req->getPath();
@ -36,6 +58,14 @@ class Router {
->send();
}
/**
* Führt die definierten Middlewares für eine Anfrage aus.
*
* @param array $middlewares Liste der Middlewares.
* @param Request $req Die eingehende HTTP-Anfrage.
* @param Response $res Die HTTP-Antwort.
* @return bool Gibt true zurück, wenn alle Middlewares erfolgreich ausgeführt wurden, andernfalls false.
*/
private function handleMiddlewares(array $middlewares, Request $req, Response $res): bool {
foreach($middlewares as $middleware) {
$middlewareInstance = is_string($middleware) ? new $middleware() : $middleware;

View File

@ -1,19 +0,0 @@
<?php
namespace Blog\Core;
use Exception;
class Container {
private array $instances = [];
public function set(string $key, callable $factory): void {
$this->instances[$key] = $factory;
}
public function get(string $key): mixed {
if(!isset($this->instances[$key])) {
throw new Exception("No instance found for {$key}");
}
return $this->instances[$key]($this);
}
}

View File

@ -4,9 +4,21 @@ namespace Blog\Database;
use PDO;
use PDOException;
/**
* Verwaltet die Verbindung zur MySQL-Datenbank.
*/
class Database {
/**
* @var PDO|null Statische Instanz der Datenbankverbindung.
*/
private static ?PDO $pdo = null;
/**
* Stellt eine Verbindung zur Datenbank her oder gibt die bestehende zurück.
*
* @return PDO Die aktive PDO-Verbindung.
* @throws PDOException Falls die Verbindung fehlschlägt.
*/
public static function getConnection(): PDO {
if(self::$pdo === null) {
$config = parse_ini_file(__DIR__ . "/../../.env");

72
app/Entity/Post.php Normal file
View File

@ -0,0 +1,72 @@
<?php
namespace Blog\Entity;
/**
* Repräsentiert einen Blogpost mit ID, Titel, Inhalt, Autor und Zeitstempel.
*/
class Post {
/**
* Erstellt eine neue Post-Instanz.
*
* @param int $id Die eindeutige ID des Blogposts.
* @param string $title Der Titel des Blogposts.
* @param string $content Der Inhalt des Blogposts.
* @param string $author Der Name des Autors.
* @param int $stamp Der Zeitstempel der Veröffentlichung.
*/
public function __construct(
private int $id,
private string $title,
private string $content,
private string $author,
private int $stamp
) {}
/**
* Gibt die ID des Blogposts zurück.
*
* @return int Die eindeutige ID.
*/
public function getId(): int {
return $this->id;
}
/**
* Gibt den Titel des Blogposts zurück.
*
* @return string Der Titel des Blogposts.
*/
public function getTitle(): string {
return $this->title;
}
/**
* Gibt den Inhalt des Blogposts zurück. Optional kann eine maximale Länge angegeben werden.
*
* @param int|null $maxlength Die maximale Zeichenanzahl, falls angegeben.
* @return string Der gekürzte oder vollständige Inhalt des Blogposts.
*/
public function getContent($maxlength = null): string {
return $maxlength
? mb_strimwidth($this->content, 0, $maxlength, "...")
: $this->content;
}
/**
* Gibt den Namen des Autors zurück.
*
* @return string Der Name des Autors.
*/
public function getAuthor(): string {
return $this->author;
}
/**
* Gibt das Veröffentlichungsdatum und die Uhrzeit formatiert zurück.
*
* @return string Das Datum und die Uhrzeit im Format `d.m.Y H:i:s`.
*/
public function getDateTime(): string {
return date('d.m.Y H:i:s', $this->stamp);
}
}

47
app/Entity/User.php Normal file
View File

@ -0,0 +1,47 @@
<?php
namespace Blog\Entity;
/**
* Repräsentiert einen Benutzer mit ID, Benutzernamen und Passwort.
*/
class User {
/**
* Erstellt eine neue User-Instanz.
*
* @param int $id Die eindeutige ID des Benutzers.
* @param string $username Der Benutzername.
* @param string $password Das Passwort des Benutzers.
*/
public function __construct(
private int $id,
private string $username,
private string $password
) {}
/**
* Gibt die ID des Benutzers zurück.
*
* @return int Die eindeutige ID des Benutzers.
*/
public function getId(): int {
return $this->id;
}
/**
* Gibt den Benutzernamen zurück.
*
* @return string Der Benutzername.
*/
public function getUsername(): string {
return $this->username;
}
/**
* Gibt das Passwort des Benutzers zurück.
*
* @return string Das Passwort.
*/
public function getPassword(): string {
return $this->password;
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace Blog\Entity;
class Post {
public function __construct(
private int $id,
private string $title,
private string $content,
private string $author,
private int $stamp
) {}
public function getId() {
return $this->id;
}
public function getTitle() {
return $this->title;
}
public function getContent($maxlength = null) {
return $maxlength ? mb_strimwidth($this->content, 0, $maxlength, "...") : $this->content;
}
public function getAuthor() {
return $this->author;
}
public function getDateTime() {
return $this->stamp;
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace Blog\Entity;
class User {
private $id;
private $username;
private $password;
public function __construct($id, $username, $password) {
$this->id = $id;
$this->username = $username;
$this->password = $password;
}
public function getId() {
return $this->id;
}
public function getUsername() {
return $this->username;
}
public function getPassword() {
return $this->password;
}
}

106
app/Http/Request.php Normal file
View File

@ -0,0 +1,106 @@
<?php
namespace Blog\Http;
/**
* Stellt eine HTTP-Request-Objektklasse bereit.
*
* Kapselt HTTP-Methoden, Pfad, POST-Daten, Header und Rohdatenzugriff.
*/
class Request {
private string $method;
private string $path;
private array $postData;
/**
* Konstruktor für das Request-Objekt.
*
* @param string $method HTTP-Methode (z.B. GET, POST)
* @param string $uri URI der Anfrage
* @param array $postData Optional: POST-Daten (Standard: $_POST)
*/
public function __construct(string $method, string $uri, array $postData = []) {
$this->method = strtoupper($method);
$this->path = parse_url($uri, PHP_URL_PATH) ?? '/';
$this->postData = $postData ?: $_POST;
}
/**
* Gibt die HTTP-Methode zurück.
*
* @return string
*/
public function getMethod(): string {
return $this->method;
}
/**
* Gibt den Pfad der Anfrage zurück.
*
* @return string
*/
public function getPath(): string {
return $this->path;
}
/**
* Gibt einen POST-Wert anhand des Schlüssels zurück.
*
* @param string $key Schlüssel im POST-Array
* @param mixed $default Optionaler Standardwert
* @return mixed
*/
public function getPost(string $key, $default = null): mixed {
return $this->postData[$key] ?? $default;
}
/**
* Gibt alle POST-Daten als Array zurück.
*
* @return array
*/
public function allPost(): array {
return $this->postData;
}
/**
* Gibt einen Query-Parameter anhand des Schlüssels zurück.
*
* @param string $key Schlüssel im Query-String
* @param mixed $default Optionaler Standardwert
* @return mixed
*/
public function getQuery(string $key, $default = null): mixed {
$query = [];
parse_str(parse_url($this->path, PHP_URL_QUERY) ?? '', $query);
return $query[$key] ?? $default;
}
/**
* Gibt einen HTTP-Header zurück.
*
* @param string $key Header-Name (z.B. 'Content-Type')
* @return string|null
*/
public function getHeader(string $key): ?string {
$headerKey = 'HTTP_' . strtoupper(str_replace('-', '_', $key));
return $_SERVER[$headerKey] ?? null;
}
/**
* Gibt den rohen Anfrage-Body zurück.
*
* @return string
*/
public function getRawInput(): string {
return file_get_contents('php://input');
}
/**
* Gibt den Anfrage-Body als assoziatives Array zurück (JSON).
*
* @return array
*/
public function getJson(): array {
return json_decode($this->getRawInput(), true) ?? [];
}
}

95
app/Http/Response.php Normal file
View File

@ -0,0 +1,95 @@
<?php
namespace Blog\Http;
/**
* Stellt eine HTTP-Response-Objektklasse bereit.
*
* Kapselt Statuscode, Header, Body und Methoden zum Senden von Antworten.
*/
class Response {
private int $status = 200;
private array $headers = [];
private string $body = "";
/**
* Setzt den HTTP-Statuscode.
*
* @param int $status HTTP-Statuscode
* @return self
*/
public function setStatus(int $status): self {
$this->status = $status;
return $this;
}
/**
* Fügt einen HTTP-Header hinzu.
*
* @param string $key Header-Name
* @param string $val Header-Wert
* @return self
*/
public function addHeader(string $key, string $val): self {
$this->headers[$key] = $val;
return $this;
}
/**
* Gibt das Response-Objekt zurück (für Method Chaining).
*
* @return self
*/
public function getBody(): self {
return $this;
}
/**
* Fügt dem Body Inhalt hinzu.
*
* @param string $content Inhalt, der angehängt wird
* @return self
*/
public function write(string $content): self {
$this->body .= $content;
return $this;
}
/**
* Sendet die HTTP-Antwort an den Client.
*
* @return void
*/
public function send(): void {
http_response_code($this->status);
foreach($this->headers as $key => $val)
header("{$key}: {$val}");
echo $this->body;
}
/**
* Sendet eine JSON-Antwort.
*
* @param array $data Zu sendende Daten
* @param int $status Optionaler HTTP-Statuscode (Standard: 200)
* @return self
*/
public function json(array $data, int $status = 200): self {
$this->setStatus($status);
header("Content-Type: application/json");
$this->body = json_encode($data);
return $this;
}
/**
* Führt eine HTTP-Weiterleitung durch.
*
* @param string $url Ziel-URL
* @param int $status Optionaler Statuscode (Standard: 302)
* @return void
*/
public function redirect(string $url, int $status = 302): void {
http_response_code($status);
header("Location: {$url}");
exit;
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace Blog\Http;
class Request {
private string $method;
private string $path;
private array $postData;
public function __construct(string $method, string $uri, array $postData = []) {
$this->method = strtoupper($method);
$this->path = parse_url($uri, PHP_URL_PATH) ?? '/';
$this->postData = $postData ?: $_POST;
}
public function getMethod(): string {
return $this->method;
}
public function getPath(): string {
return $this->path;
}
public function getPost(string $key, $default = null): mixed {
return $this->postData[$key] ?? $default;
}
public function allPost(): array {
return $this->postData;
}
public function getQuery(string $key, $default = null): mixed {
$query = [];
parse_str(parse_url($this->path, PHP_URL_QUERY) ?? '', $query);
return $query[$key] ?? $default;
}
public function getHeader(string $key): ?string {
$headerKey = 'HTTP_' . strtoupper(str_replace('-', '_', $key));
return $_SERVER[$headerKey] ?? null;
}
public function getRawInput(): string {
return file_get_contents('php://input');
}
public function getJson(): array {
return json_decode($this->getRawInput(), true) ?? [];
}
}

View File

@ -1,47 +0,0 @@
<?php
namespace Blog\Http;
class Response {
private int $status = 200;
private array $headers = [];
private string $body = "";
public function setStatus(int $status): self {
$this->status = $status;
return $this;
}
public function addHeader(string $key, string $val): self {
$this->headers[$key] = $val;
return $this;
}
public function getBody(): self {
return $this;
}
public function write(string $content): self {
$this->body .= $content;
return $this;
}
public function send(): void {
http_response_code($this->status);
foreach($this->headers as $key => $val)
header("{$key}: {$val}");
echo $this->body;
}
public function json(array $data, int $status = 200): self {
$this->setStatus($status);
header("Content-Type: application/json");
$this->body = json_encode($data);
return $this;
}
public function redirect(string $url, int $status = 302): void {
http_response_code($status);
header("Location: {$url}");
exit;
}
}

View File

@ -1,11 +1,23 @@
<?php
namespace Blog\Middleware;
use Blog\Middleware\middlewareInterface;
use Blog\Http\request;
use Blog\Http\response;
use Blog\Middleware\MiddlewareInterface;
use Blog\Http\Request;
use Blog\Http\Response;
/**
* Middleware zur Authentifizierung und CSRF-Prüfung.
*
* Prüft, ob ein Benutzer eingeloggt ist und ob ein gültiges CSRF-Token vorliegt.
*/
class AuthMiddleware implements MiddlewareInterface {
/**
* Führt die Authentifizierungs- und CSRF-Prüfung durch.
*
* @param Request $request Das aktuelle Request-Objekt
* @param Response $response Das aktuelle Response-Objekt
* @return bool true, wenn die Anfrage fortgesetzt werden darf, sonst false
*/
public function handle(Request $request, Response $response): bool {
if(!isset($_SESSION['user'])) {
$response
@ -28,11 +40,22 @@ class AuthMiddleware implements MiddlewareInterface {
return true;
}
private function validateCSRFToken(Request $request): bool {
/**
* Prüft, ob das CSRF-Token gültig ist.
*
* @param Request $request Das aktuelle Request-Objekt
* @return bool true, wenn das Token gültig ist, sonst false
*/
public static function validateCSRFToken(Request $request): bool {
$token = $request->getPost('_csrf_token') ?? '';
return hash_equals($_SESSION['_csrf_token'] ?? '', $token);
}
/**
* Generiert und gibt ein CSRF-Token zurück.
*
* @return string Das generierte oder vorhandene CSRF-Token
*/
public static function generateCSRFToken(): string {
if(!isset($_SESSION['_csrf_token'])) {
$_SESSION['_csrf_token'] = bin2hex(random_bytes(32));

View File

@ -0,0 +1,21 @@
<?php
namespace Blog\Middleware;
use Blog\Http\Request;
use Blog\Http\Response;
/**
* Interface für Middleware-Komponenten.
*
* Definiert eine Methode zur Bearbeitung von HTTP-Anfragen und -Antworten.
*/
interface MiddlewareInterface {
/**
* Bearbeitet die eingehende Anfrage und Antwort.
*
* @param Request $request Das aktuelle Request-Objekt
* @param Response $response Das aktuelle Response-Objekt
* @return bool true, wenn die Anfrage fortgesetzt werden darf, sonst false
*/
public function handle(Request $request, Response $response): bool;
}

View File

@ -1,9 +0,0 @@
<?php
namespace Blog\Middleware;
use Blog\Http\request;
use Blog\Http\response;
interface MiddlewareInterface {
public function handle(Request $request, Response $response): bool;
}

117
app/Model/PostModel.php Normal file
View File

@ -0,0 +1,117 @@
<?php
namespace Blog\Model;
use Blog\Entity\Post;
use Blog\Database\Database;
use Exception;
class PostModel {
private $db;
public function __construct() {
$this->db = Database::getConnection();
}
/**
* Gibt alle Blogposts als Array von Post-Objekten zurück.
*
* @return Post[] Array mit allen Blogposts.
* @throws Exception Wenn keine Einträge vorhanden sind.
*/
public function getPosts(): array {
$posts = [];
$query = $this->db->query(<<<SQL
SELECT p.id, u.username, p.title, p.content, p.stamp
FROM posts p
JOIN users u ON u.id = p.author_id
ORDER BY p.id DESC
SQL);
if(!$query->rowCount())
throw new Exception("no entries available");
while($row = $query->fetchObject()) {
$posts[] = new Post($row->id, $row->title, $row->content, $row->username, $row->stamp);
}
return $posts;
}
/**
* Gibt einen einzelnen Blogpost anhand der ID zurück.
*
* @param int $id Die ID des Blogposts.
* @return Post|null Das Post-Objekt oder null, falls nicht gefunden.
* @throws Exception Wenn kein Eintrag gefunden wurde.
*/
public function getPost($id): ?Post {
$stmt = $this->db->prepare(<<<SQL
SELECT p.id, u.username, p.title, p.content, p.stamp
FROM posts p
JOIN users u ON u.id = p.author_id
WHERE p.id = :id
LIMIT 1
SQL);
$stmt->bindParam(":id", $id, $this->db::PARAM_INT);
$stmt->execute();
if(!$stmt->rowCount())
throw new Exception("no entry found");
$row = $stmt->fetchObject();
return new Post($row->id, $row->title, $row->content, $row->username, $row->stamp);
}
/**
* Aktualisiert den Inhalt eines Blogposts.
*
* @param int $id Die ID des Blogposts.
* @param string $content Der neue Inhalt.
* @return bool Erfolg der Aktualisierung.
*/
public function updatePostContent($id, $content) {
$stmt = $this->db->prepare(<<<SQL
UPDATE posts
SET content = :content
WHERE id = :id
SQL);
$stmt->bindValue(':content', $content, \PDO::PARAM_STR);
$stmt->bindValue(':id', $id, \PDO::PARAM_INT);
return $stmt->execute();
}
/**
* Legt einen neuen Blogpost an.
*
* @param string $title Der Titel des Blogposts.
* @param string $content Der Inhalt des Blogposts.
* @param int $authorId Die ID des Autors.
* @return bool Erfolg des Einfügens.
*/
public function createPost($title, $content, $authorId) {
$stmt = $this->db->prepare(<<<SQL
INSERT INTO posts (title, content, author_id, stamp)
VALUES
(:title, :content, :author_id, :stamp)
SQL);
$stmt->bindValue(':title', $title, \PDO::PARAM_STR);
$stmt->bindValue(':content', $content, \PDO::PARAM_STR);
$stmt->bindValue(':author_id', $authorId, \PDO::PARAM_INT);
$stmt->bindValue(':stamp', time(), \PDO::PARAM_INT);
return $stmt->execute();
}
/**
* Löscht einen Blogpost anhand der ID.
*
* @param int $id Die ID des Blogposts.
* @return bool Erfolg des Löschens.
*/
public function deletePost($id) {
$stmt = $this->db->prepare(<<<SQL
DELETE FROM posts
WHERE id = :id
SQL);
$stmt->bindValue(':id', $id, \PDO::PARAM_INT);
return $stmt->execute();
}
}

46
app/Model/UserModel.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace Blog\Model;
use Blog\Entity\User;
use Blog\Database\Database;
use PDO;
/**
* Modellklasse für Benutzer-bezogene Datenbankoperationen.
*/
class UserModel {
/**
* @var PDO Die Datenbankverbindung.
*/
private $db;
/**
* Konstruktor. Stellt die Datenbankverbindung her.
*/
public function __construct() {
$this->db = Database::getConnection();
}
/**
* Holt einen Benutzer anhand des Benutzernamens.
*
* @param string $username Der Benutzername.
* @return User|null Das User-Objekt oder null, falls nicht gefunden.
*/
public function getUserByUsername(string $username):?User {
$stmt = $this->db->prepare(<<<SQL
SELECT id, username, password
FROM users
WHERE username = :username
LIMIT 1
SQL);
$stmt->bindParam(':username', $username);
$stmt->execute();
$row = $stmt->fetchObject();
if(!$row)
return null;
return new User($row->id, $row->username, $row->password);
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace Blog\Model;
use Blog\Entity\post;
use Blog\Database\database;
use Exception;
class PostModel {
private $db;
public function __construct() {
$this->db = Database::getConnection();
}
public function getPosts(): array {
$posts = [];
$query = $this->db->query(<<<SQL
SELECT p.id, u.username, p.title, p.content, p.stamp
FROM posts p
JOIN users u ON u.id = p.author_id
SQL);
if(!$query->rowCount())
throw new Exception("no entries available");
while($row = $query->fetchObject()) {
$posts[] = new Post($row->id, $row->title, $row->content, $row->username, $row->stamp);
}
return $posts;
}
public function getPost($id): ?Post {
$query = $this->db->prepare(<<<SQL
SELECT p.id, u.username, p.title, p.content, p.stamp
FROM posts p
JOIN users u ON u.id = p.author_id
WHERE p.id = :id
LIMIT 1
SQL);
$query->bindParam(":id", $id, $this->db::PARAM_INT);
$query->execute();
if(!$query->rowCount())
throw new Exception("no entry found");
$row = $query->fetchObject();
return new Post($row->id, $row->title, $row->content, $row->username, $row->stamp);
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace Blog\Model;
use Blog\Entity\user;
use Blog\Database\database;
use PDO;
class UserModel {
private $db;
public function __construct() {
$this->db = Database::getConnection();
}
public function getUserByUsername(string $username):?User {
$query = $this->db->prepare(<<<SQL
SELECT id, username, password
FROM users
WHERE username = :username
LIMIT 1
SQL);
$query->bindParam(':username', $username);
$query->execute();
$row = $query->fetchObject();
if(!$row)
return null;
return new User($row->id, $row->username, $row->password);
}
}

156
app/Template/Twig.php Normal file
View File

@ -0,0 +1,156 @@
<?php
namespace Blog\Template;
use Exception;
/**
* Einfache Template-Engine zur Verarbeitung von Twig-ähnlichen Templates.
*/
class Twig {
/**
* @var array Enthält die erkannten Template-Blöcke.
*/
private array $blocks = [];
/**
* @var string Pfad zum Template-Verzeichnis.
*/
private string $viewsPath;
/**
* @var array Globale Variablen, die in allen Templates verfügbar sind.
*/
private array $globals = [];
/**
* Konstruktor.
*
* @param string $viewsPath Pfad zum Template-Verzeichnis.
*/
public function __construct($viewsPath) {
$this->viewsPath = rtrim($viewsPath, '/') . '/';
}
/**
* Setzt globale Variablen für alle Templates.
*
* @param array $globals Assoziatives Array mit globalen Variablen.
*/
public function setGlobals(array $globals) {
$this->globals = $globals;
}
/**
* Rendert ein Template mit den übergebenen Daten.
*
* @param string $file Name der Template-Datei (ohne Pfad).
* @param array $data Assoziatives Array mit Variablen für das Template.
* @return string Gerenderter HTML-Code.
* @throws Exception Wenn das Template nicht gefunden oder ein Fehler auftritt.
*/
public function render($file, $data = []): string {
$data = array_merge($this->globals, $data);
try {
$code = $this->includeFiles($file);
$code = $this->compileCode($code);
extract($data, EXTR_SKIP);
ob_start();
eval('?>' . $code);
return ob_get_clean();
} catch(Exception $e) {
throw new Exception("Error rendering template: " . $e->getMessage());
}
}
/**
* Wandelt den Template-Code in ausführbaren PHP-Code um.
*
* @param string $code Der Template-Code.
* @return string Kompilierter PHP-Code.
*/
private function compileCode($code): string {
$code = $this->compileBlock($code);
$code = $this->compileYield($code);
$code = $this->compileEchos($code);
$code = $this->compilePHP($code);
return $code;
}
/**
* Fügt eingebundene oder erweiterte Templates ein.
*
* @param string $file Name der Template-Datei.
* @return string Template-Code mit eingebundenen Dateien.
* @throws Exception Wenn die Datei nicht gefunden wird.
*/
private function includeFiles($file): string {
$filePath = $this->viewsPath . preg_replace("/\.twig$/", "", $file) . ".twig";
if(!file_exists($filePath)) {
throw new Exception("View file not found: {$filePath}");
}
$code = file_get_contents($filePath);
preg_match_all('/{% ?(extends|include) ?\'?(.*?)\'? ?%}/i', $code, $matches, PREG_SET_ORDER);
foreach($matches as $match) {
$includedCode = $this->includeFiles($match[2]);
$code = str_replace($match[0], $includedCode, $code);
}
return preg_replace('/{% ?(extends|include) ?\'?(.*?)\'? ?%}/i', '', $code);
}
/**
* Wandelt {% ... %} in PHP-Code um.
*
* @param string $code Der Template-Code.
* @return string PHP-Code.
*/
private function compilePHP($code): string {
return preg_replace('~\{%\s*(.+?)\s*%}~is', '<?php $1 ?>', $code);
}
/**
* Wandelt {{ ... }} in PHP-Echo-Ausgaben um.
*
* @param string $code Der Template-Code.
* @return string PHP-Code mit Echo-Ausgaben.
*/
private function compileEchos($code): string {
$code = preg_replace('~\{\{\{\s*(.+?)\s*\}\}\}~is', '<?=htmlspecialchars($1, ENT_QUOTES, "UTF-8")?>', $code);
return preg_replace('~\{\{\s*(.+?)\s*\}\}~is', '<?=$1?>', $code);
}
/**
* Sammelt und entfernt Block-Definitionen aus dem Template.
*
* @param string $code Der Template-Code.
* @return string Template-Code ohne Block-Definitionen.
*/
private function compileBlock($code): string {
preg_match_all('/{% ?block ?(.*?) ?%}(.*?){% ?endblock ?%}/is', $code, $matches, PREG_SET_ORDER);
foreach($matches as $match) {
if(!isset($this->blocks[$match[1]])) {
$this->blocks[$match[1]] = '';
}
$this->blocks[$match[1]] = str_replace('@parent', $this->blocks[$match[1]], $match[2]);
$code = str_replace($match[0], '', $code);
}
return $code;
}
/**
* Ersetzt yield-Platzhalter durch Block-Inhalte.
*
* @param string $code Der Template-Code.
* @return string Template-Code mit ersetzten Yields.
*/
private function compileYield($code): string {
foreach($this->blocks as $block => $value) {
$code = preg_replace('/{% ?yield ?' . $block . ' ?%}/', $value, $code);
}
return preg_replace('/{% ?yield ?(.*?) ?%}/i', '', $code);
}
}

View File

@ -1,88 +0,0 @@
<?php
namespace Blog\Template;
use Exception;
class Twig {
private array $blocks = [];
private string $viewsPath;
public function __construct($viewsPath) {
$this->viewsPath = rtrim($viewsPath, '/') . '/';
}
public function render($file, $data = []): string {
try {
$code = $this->includeFiles($file);
$code = $this->compileCode($code);
extract($data, EXTR_SKIP);
ob_start();
eval('?>' . $code);
return ob_get_clean();
} catch(Exception $e) {
throw new Expection("Error rendering template: " . $e->getMessage());
}
}
private function compileCode($code): string {
$code = $this->compileBlock($code);
$code = $this->compileYield($code);
$code = $this->compileEchos($code);
$code = $this->compilePHP($code);
return $code;
}
private function includeFiles($file): string {
$filePath = $this->viewsPath . preg_replace("/\.twig$/", "", $file) . ".twig";
if(!file_exists($filePath)) {
throw new Exception("View file not found: {$filePath}");
}
$code = file_get_contents($filePath);
preg_match_all('/{% ?(extends|include) ?\'?(.*?)\'? ?%}/i', $code, $matches, PREG_SET_ORDER);
foreach($matches as $match) {
$includedCode = $this->includeFiles($match[2]);
$code = str_replace($match[0], $includedCode, $code);
}
return preg_replace('/{% ?(extends|include) ?\'?(.*?)\'? ?%}/i', '', $code);
}
private function compilePHP($code): string {
return preg_replace('~\{%\s*(.+?)\s*%}~is', '<?php $1 ?>', $code);
}
private function compileEchos($code): string {
$code = preg_replace('~\{\{\s*(.+?)\s*\}\}~is', '<?=$1?>', $code);
return preg_replace('~\{\{\{\s*(.+?)\s*\}\}\}~is', '<?=htmlspecialchars($1, ENT_QUOTES, "UTF-8")?>', $code);
}
private function compileEscapedEchos($code):string {
return preg_replace('~\{{{\s*(.+?)\s*}}}~is', '<?=htmlentities($1, ENT_QUOTES, "UTF-8")?>', $code);
}
/*
*
*
*/
private function compileBlock($code): string {
preg_match_all('/{% ?block ?(.*?) ?%}(.*?){% ?endblock ?%}/is', $code, $matches, PREG_SET_ORDER);
foreach($matches as $match) {
if(!isset($this->blocks[$match[1]])) {
$this->blocks[$match[1]] = '';
}
$this->blocks[$match[1]] = str_replace('@parent', $this->blocks[$match[1]], $match[2]);
$code = str_replace($match[0], '', $code);
}
return $code;
}
private function compileYield($code): string {
foreach($this->blocks as $block => $value) {
$code = preg_replace('/{% ?yield ?' . $block . ' ?%}/', $value, $code);
}
return preg_replace('/{% ?yield ?(.*?) ?%}/i', '', $code);
}
}

View File

@ -1,16 +1,36 @@
<?php
namespace Blog\Utils;
use Blog\Model\userModel;
use Blog\Model\UserModel;
use Exception;
/**
* Hilfsklasse für Authentifizierung und Session-Management.
*/
class AuthHelper {
/**
* @var UserModel|null
*/
private static ?UserModel $userModel = null;
/**
* Initialisiert den AuthHelper mit einem UserModel.
*
* @param UserModel $userModel
* @return void
*/
public static function initialize(UserModel $userModel): void {
self::$userModel = $userModel;
}
/**
* Versucht, einen Benutzer mit Benutzername und Passwort einzuloggen.
*
* @param string $username
* @param string $password
* @return bool True bei Erfolg, sonst false.
* @throws Exception Wenn das UserModel nicht initialisiert wurde.
*/
public static function login(string $username, string $password): bool {
if(!self::$userModel)
throw new Exception("AuthHelper was not initialized. Please pass UserModel.");
@ -28,15 +48,30 @@ class AuthHelper {
return true;
}
/**
* Loggt den aktuellen Benutzer aus und zerstört die Session.
*
* @return void
*/
public static function logout(): void {
session_unset();
session_destroy();
}
/**
* Prüft, ob ein Benutzer eingeloggt ist.
*
* @return bool True, wenn ein Benutzer eingeloggt ist, sonst false.
*/
public static function isLoggedIn(): bool {
return isset($_SESSION['user']);
}
/**
* Gibt die aktuellen Benutzerdaten aus der Session zurück.
*
* @return array|null Benutzerdaten oder null, falls nicht eingeloggt.
*/
public static function getCurrentUser(): ?array {
return $_SESSION['user'] ?? null;
}