diff --git a/.dockerignore b/.dockerignore index cbd5db9..ad20426 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,8 @@ **/__pycache__ **/*.pyc frontend/dist -creator.db +storage +projects +guides.db .git -.env +.claude-data diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8147a44 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Datei nach .env kopieren (wird nicht committet). + +# Claude-Provider: lokal einmal 'claude setup-token' ausführen, Token eintragen. +CLAUDE_CODE_OAUTH_TOKEN= + +# MiniMax-Provider: API-Key aus der MiniMax-Console (Coding-Plan). +MINIMAX_API_KEY= diff --git a/.gitignore b/.gitignore index 955363b..1d0fe65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ +storage/ +projects/ creator.db node_modules/ frontend/dist/ __pycache__/ *.pyc +.claude-data/ .env diff --git a/Dockerfile b/Dockerfile index 24ef687..cb2f81a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,13 +9,25 @@ RUN npm run build # Stage 2: Runtime FROM python:3.12-slim +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + gnupg \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g @anthropic-ai/claude-code opencode-ai \ + && pip install --no-cache-dir uv \ + && rm -rf /var/lib/apt/lists/* + RUN useradd -m -u 1000 app COPY backend/requirements.txt /app/backend/requirements.txt RUN pip install --no-cache-dir -r /app/backend/requirements.txt COPY --chown=app:app backend/ /app/backend/ +COPY --chown=app:app templates/ /app/templates/ COPY --chown=app:app --from=frontend /build/dist /app/frontend/dist +COPY --chown=app:app dev-ops/opencode.json /home/app/.config/opencode/opencode.json RUN chown app:app /app diff --git a/Makefile b/Makefile index 3b7b319..1f6e18b 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,36 @@ -.PHONY: install dev prod stop logs +.PHONY: install dev prod stop logs remove auth sync COMPOSE = docker compose +auth: + @mkdir -p .claude-data storage projects + @chown -R 1000:1000 .claude-data storage projects + @grep -q "^CLAUDE_CODE_OAUTH_TOKEN=.\+" .env 2>/dev/null \ + || echo "WARNUNG: CLAUDE_CODE_OAUTH_TOKEN fehlt in .env (Claude-Provider inaktiv)." + @grep -q "^MINIMAX_API_KEY=.\+" .env 2>/dev/null \ + || echo "WARNUNG: MINIMAX_API_KEY fehlt in .env (MiniMax-Provider inaktiv)." + @echo "Verzeichnisse angelegt und auf uid 1000 chowned." + install: - pip install --break-system-packages -r backend/requirements.txt + pip install --break-system-packages fastapi uvicorn[standard] aiosqlite uv cd frontend && npm install + npm install -g opencode-ai + @mkdir -p $(HOME)/.config/opencode + @ln -sfn $(CURDIR)/dev-ops/opencode.json $(HOME)/.config/opencode/opencode.json + @if grep -q "^MINIMAX_API_KEY=.\+" .env 2>/dev/null; then \ + echo "OpenCode-Config verlinkt. MINIMAX_API_KEY aus .env wird von 'make dev' geladen."; \ + else \ + echo "OpenCode-Config verlinkt. MINIMAX_API_KEY noch in .env eintragen."; \ + fi dev: @echo "Backend: http://localhost:8000" @echo "Frontend: http://localhost:5173" - @cd backend && uvicorn main:app --reload --port 8000 & + @set -a; [ -f .env ] && . ./.env; set +a; \ + cd backend && uvicorn main:app --reload --port 8000 & @cd frontend && npx vite --port 5173 -prod: - @touch creator.db +prod: auth $(COMPOSE) up -d --build stop: @@ -24,3 +41,14 @@ stop: logs: $(COMPOSE) logs -f + +remove: stop + @echo "Lösche Datenbank und generierte Dateien..." + rm -rf storage/* + @echo "Fertig." + +sync: + @mkdir -p storage/html + rsync -avz --progress root@178.104.67.87:/var/www/creator/storage/creator.db storage/ + rsync -avz --progress --delete root@178.104.67.87:/var/www/creator/storage/html/ storage/html/ + @echo "Sync abgeschlossen." diff --git a/README.md b/README.md new file mode 100644 index 0000000..50af4d8 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +Bausteine finden +- 1+ Agenten suchen Bausteine zum Thema +- Baustein bekommt die Einstufung kern, wichtig und rest + +MiniGuide generieren +- 1 Agent erstellt den Guide +- Nur Themen (kern) verwenden +- MiniGuide-Format bestimmt den Stil + +Guide generieren +- 1 Agent erstellt den Guide +- Nur Themen (kern+wichtig) verwenden +- Guide-Format bestimmt den Stil + +Flow +- Bausteine finden +- HTML erstellen +- Code prüfen \ No newline at end of file diff --git a/backend/agents.py b/backend/agents.py new file mode 100644 index 0000000..411893c --- /dev/null +++ b/backend/agents.py @@ -0,0 +1,139 @@ +"""Provider-Schicht: führt Agent-Aufrufe über die Claude-CLI oder OpenCode (MiniMax) aus. + +Beide Runner sind unabhängig. Fehlt ein Binary/Key, schlägt nur der +jeweilige Provider fehl — der andere läuft unverändert weiter. +""" + +import asyncio +import os +import re +import shutil +import tempfile +from pathlib import Path + +from config import PROVIDERS, DEFAULT_PROVIDER + +_active_processes: dict[str, asyncio.subprocess.Process] = {} + +# Capability → Claude --allowedTools +_CLAUDE_TOOLS = { + "full": "Write,Bash,Read,WebSearch,WebFetch", + "files": "Read,Bash,Write", + "read": "Read", + "none": None, +} + +# Capability → OpenCode-Agent (Tool-Rechte in dev-ops/opencode.json definiert) +_OPENCODE_AGENTS = { + "full": "full", + "files": "files", + "read": "readonly", + "none": "text", +} + + +def provider_available(provider: str) -> bool: + cfg = PROVIDERS.get(provider) + if not cfg: + return False + if shutil.which(cfg["cli"]) is None: + return False + env_key = cfg.get("env_key") + if env_key and not os.environ.get(env_key): + return False + return True + + +def kill_process(agent_key: str) -> None: + process = _active_processes.get(agent_key) + if process and process.returncode is None: + process.kill() + + +async def run_agent( + agent_key: str, + prompt: str, + timeout: int, + provider: str = DEFAULT_PROVIDER, + role: str = "fast", + capabilities: str = "none", +) -> tuple[int, str, str]: + if provider not in PROVIDERS: + return 1, "", f"Unbekannter Provider: {provider}" + if shutil.which(PROVIDERS[provider]["cli"]) is None: + return 1, "", f"CLI '{PROVIDERS[provider]['cli']}' nicht installiert (Provider: {provider})" + timeout = int(timeout * PROVIDERS[provider].get("timeout_factor", 1)) + if provider == "minimax": + return await _run_opencode(agent_key, prompt, timeout, role, capabilities) + return await _run_claude_cli(agent_key, prompt, timeout, role, capabilities) + + +async def _communicate(agent_key: str, cmd: list[str], stdin_data: bytes | None, timeout: int) -> tuple[int, str, str]: + process = await asyncio.create_subprocess_exec( + *cmd, + stdin=asyncio.subprocess.PIPE if stdin_data is not None else asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + _active_processes[agent_key] = process + try: + try: + stdout, stderr = await asyncio.wait_for( + process.communicate(input=stdin_data), + timeout=timeout, + ) + except asyncio.TimeoutError: + process.kill() + try: + await asyncio.wait_for(process.wait(), timeout=5) + except asyncio.TimeoutError: + pass + raise + return process.returncode, stdout.decode("utf-8", errors="replace"), stderr.decode("utf-8", errors="replace") + finally: + _active_processes.pop(agent_key, None) + + +async def _run_claude_cli(agent_key: str, prompt: str, timeout: int, role: str, capabilities: str) -> tuple[int, str, str]: + cfg = PROVIDERS["claude"] + cmd = [cfg["cli"], "-p", "--model", cfg[role]] + tools = _CLAUDE_TOOLS.get(capabilities) + if tools: + cmd += ["--allowedTools", tools] + cmd += ["--dangerously-skip-permissions"] + return await _communicate(agent_key, cmd, prompt.encode("utf-8"), timeout) + + +async def _run_opencode(agent_key: str, prompt: str, timeout: int, role: str, capabilities: str) -> tuple[int, str, str]: + cfg = PROVIDERS["minimax"] + # Prompt über Tempdatei statt argv (ARG_MAX-Schutz bei großen Projekt-Prompts) + with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8", dir=tempfile.gettempdir()) as f: + f.write(prompt) + prompt_path = Path(f.name) + # Positional-Message MUSS vor -f stehen: -f ist ein Array-Flag und + # frisst sonst den Text als zweiten Dateinamen ("File not found"). + cmd = [ + cfg["cli"], "run", + "Folge exakt den Anweisungen in der angehängten Datei. Sie sind der vollständige Auftrag.", + "-m", cfg[role], + "--agent", _OPENCODE_AGENTS.get(capabilities, "text"), + "--dangerously-skip-permissions", + "-f", str(prompt_path), + ] + try: + rc, stdout, stderr = await _communicate(agent_key, cmd, None, timeout) + return rc, _clean_opencode_output(stdout), stderr + finally: + prompt_path.unlink(missing_ok=True) + + +_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") + + +def _clean_opencode_output(text: str) -> str: + """Entfernt ANSI-Codes und den führenden Banner ("> agent · modell").""" + text = _ANSI_RE.sub("", text) + lines = text.splitlines() + while lines and (not lines[0].strip() or lines[0].lstrip().startswith(">")): + lines.pop(0) + return "\n".join(lines).strip() diff --git a/backend/config.py b/backend/config.py index 51ab814..e79bc53 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,5 +1,38 @@ from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parent.parent +TEMPLATES_DIR = PROJECT_ROOT / "templates" +STORAGE_DIR = PROJECT_ROOT / "storage" FRONTEND_DIST = PROJECT_ROOT / "frontend" / "dist" -DB_PATH = PROJECT_ROOT / "creator.db" +DB_PATH = STORAGE_DIR / "creator.db" +PROJECTS_DIR = PROJECT_ROOT / "projects" + +FORMAT_META = { + "OnePager": {"pages": "1 Seite", "time": "~5 Min"}, + "MiniGuide": {"pages": "3-5 Seiten", "time": "~15-25 Min"}, + "Guide": {"pages": "10-30 Seiten", "time": "variabel"}, +} + +AGENT_TIMEOUT = 3600 + +MAX_CONCURRENT_GENERATIONS = 10 + +# Provider-Stacks: komplett unabhängig, einer kann jederzeit entfernt werden. +# Rollen: "guide" = große Generierung, "fast" = Baustein-Recherche/Chat. +DEFAULT_PROVIDER = "claude" +PROVIDERS = { + "claude": { + "cli": "claude", + "guide": "claude-opus-4-8[1m]", + "fast": "claude-sonnet-4-6", + "env_key": None, # Auth via CLAUDE_CODE_OAUTH_TOKEN oder ~/.claude + "timeout_factor": 1, + }, + "minimax": { + "cli": "opencode", + "guide": "minimax/MiniMax-M3", + "fast": "minimax/MiniMax-M3", + "env_key": "MINIMAX_API_KEY", + "timeout_factor": 3, # M3 ist bei großen Dokumenten deutlich langsamer + }, +} diff --git a/backend/database.py b/backend/database.py index abe4b3a..339875b 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1,7 +1,29 @@ import aiosqlite - from config import DB_PATH +CREATE_GUIDES = """ +CREATE TABLE IF NOT EXISTS guides ( + id TEXT PRIMARY KEY, + topic TEXT NOT NULL, + format TEXT NOT NULL, + instructions TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'queued', + progress TEXT, + error_msg TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +) +""" + +CREATE_PROGRESS = """ +CREATE TABLE IF NOT EXISTS guide_progress ( + guide_id TEXT NOT NULL, + chapter TEXT NOT NULL, + created_at TEXT NOT NULL, + PRIMARY KEY (guide_id, chapter) +) +""" + _db: aiosqlite.Connection | None = None @@ -9,12 +31,18 @@ async def get_db() -> aiosqlite.Connection: global _db if _db is None: _db = await aiosqlite.connect(DB_PATH) + _db.row_factory = None return _db async def init_db(): db = await get_db() - # Tabellen folgen, sobald die ersten Features stehen. + await db.execute(CREATE_GUIDES) + await db.execute(CREATE_PROGRESS) + await db.execute( + "UPDATE guides SET status = 'error', progress = NULL, error_msg = 'Server-Neustart' " + "WHERE status IN ('queued', 'generating')" + ) await db.commit() @@ -28,3 +56,77 @@ async def close_db(): def _row_to_dict(row, cursor): columns = [d[0] for d in cursor.description] return dict(zip(columns, row)) + + +async def create_guide(guide: dict) -> dict: + db = await get_db() + await db.execute( + """INSERT INTO guides (id, topic, format, instructions, status, progress, created_at, updated_at) + VALUES (:id, :topic, :format, :instructions, :status, :progress, :created_at, :updated_at)""", + guide, + ) + await db.commit() + return guide + + +async def get_guide(guide_id: str) -> dict | None: + db = await get_db() + cursor = await db.execute("SELECT * FROM guides WHERE id = ?", (guide_id,)) + row = await cursor.fetchone() + if row is None: + return None + return _row_to_dict(row, cursor) + + +async def list_guides() -> list[dict]: + db = await get_db() + cursor = await db.execute("SELECT * FROM guides ORDER BY created_at DESC") + rows = await cursor.fetchall() + return [_row_to_dict(row, cursor) for row in rows] + + +async def update_guide(guide_id: str, **fields) -> None: + sets = ", ".join(f"{k} = :{k}" for k in fields) + fields["id"] = guide_id + db = await get_db() + await db.execute(f"UPDATE guides SET {sets} WHERE id = :id", fields) + await db.commit() + + +async def delete_guide(guide_id: str) -> bool: + db = await get_db() + cursor = await db.execute("DELETE FROM guides WHERE id = ?", (guide_id,)) + await db.commit() + return cursor.rowcount > 0 + + +# --- Kapitel-Fortschritt --- + +async def list_progress(guide_id: str) -> list[str]: + db = await get_db() + cursor = await db.execute( + "SELECT chapter FROM guide_progress WHERE guide_id = ?", (guide_id,) + ) + rows = await cursor.fetchall() + return [row[0] for row in rows] + + +async def set_progress(guide_id: str, chapter: str, done: bool) -> None: + from datetime import datetime, timezone + db = await get_db() + if done: + await db.execute( + "INSERT OR IGNORE INTO guide_progress (guide_id, chapter, created_at) VALUES (?, ?, ?)", + (guide_id, chapter, datetime.now(timezone.utc).isoformat()), + ) + else: + await db.execute( + "DELETE FROM guide_progress WHERE guide_id = ? AND chapter = ?", (guide_id, chapter) + ) + await db.commit() + + +async def delete_progress(guide_id: str) -> None: + db = await get_db() + await db.execute("DELETE FROM guide_progress WHERE guide_id = ?", (guide_id,)) + await db.commit() diff --git a/backend/generator.py b/backend/generator.py new file mode 100644 index 0000000..cd70208 --- /dev/null +++ b/backend/generator.py @@ -0,0 +1,178 @@ +import asyncio +import uuid +from datetime import datetime, timezone +from pathlib import Path + +from agents import run_agent, kill_process +from config import ( + AGENT_TIMEOUT, + DEFAULT_PROVIDER, + TEMPLATES_DIR, + MAX_CONCURRENT_GENERATIONS, +) +from database import update_guide +from paths import final_html_path, project_dir + +_semaphore = asyncio.Semaphore(MAX_CONCURRENT_GENERATIONS) +_cancelled: set[str] = set() + + +async def cancel_guide(guide_id: str) -> bool: + _cancelled.add(guide_id) + kill_process(guide_id) + now = datetime.now(timezone.utc).isoformat() + await update_guide(guide_id, status="error", progress=None, error_msg="Abgebrochen", updated_at=now) + return True + + +async def _set_progress(guide_id: str, progress: str) -> None: + now = datetime.now(timezone.utc).isoformat() + await update_guide(guide_id, progress=progress, updated_at=now) + + +# Welche Baustein-Kategorien jedes Format abdeckt. +FORMAT_COVERAGE = { + "OnePager": "NUR die KERN-Bausteine, maximal verdichtet", + "MiniGuide": "NUR die KERN-Bausteine", + "Guide": "die KERN- und WICHTIG-Bausteine", +} + + +def _prompt(name: str, **kwargs) -> str: + template = (TEMPLATES_DIR / "Prompt" / f"{name}.md").read_text(encoding="utf-8") + return template.format(**kwargs) + + +def _extra(instructions: str) -> str: + return f"\n\nZUSÄTZLICHE ANWEISUNGEN VOM NUTZER:\n{instructions}\n" if instructions else "" + + +def _build_bausteine_prompt(topic: str, bausteine_path: Path, instructions: str = "", project: Path | None = None) -> str: + if project: + source = _prompt("Bausteine-Quelle-Projekt", project=project) + else: + source = _prompt("Bausteine-Quelle-Thema", topic=topic) + return _prompt( + "Bausteine", + topic=topic, source=source, bausteine_path=bausteine_path, extra=_extra(instructions), + ) + + +def _build_guide_prompt(topic: str, format_name: str, html_path: Path, bausteine: str, instructions: str = "", project: Path | None = None) -> str: + spec = (TEMPLATES_DIR / "Format" / f"{format_name}.md").read_text(encoding="utf-8") + reference = (TEMPLATES_DIR / "Referenz" / f"{format_name}.md").read_text(encoding="utf-8") + + if project: + facts = _prompt("Guide-Fakten-Projekt", project=project) + else: + facts = _prompt("Guide-Fakten-Thema") + + return _prompt( + "Guide", + topic=topic, format_name=format_name, html_path=html_path, + bausteine=bausteine, coverage=FORMAT_COVERAGE[format_name], + facts=facts, spec=spec, reference=reference, extra=_extra(instructions), + ) + + +async def generate_guide(guide_id: str, topic: str, format_name: str, instructions: str = "", provider: str = DEFAULT_PROVIDER) -> None: + async with _semaphore: + now = datetime.now(timezone.utc).isoformat() + await update_guide(guide_id, status="generating", progress="Ermittle Bausteine…", updated_at=now) + + html_path = final_html_path(topic, format_name) + bausteine_path = html_path.with_suffix(".bausteine.md") + project = project_dir(topic) if project_dir(topic).is_dir() else None + + try: + if guide_id in _cancelled: + return + + # Step 1: Bausteine ermitteln (Thema: Websuche, Projekt: Dateien lesen) + current_step = "Bausteine" + bs_prompt = _build_bausteine_prompt(topic, bausteine_path, instructions, project) + returncode, bs_out, bs_err = await run_agent( + guide_id, bs_prompt, AGENT_TIMEOUT, + provider=provider, role="fast", capabilities="files" if project else "full", + ) + + if guide_id in _cancelled: + return + if returncode != 0: + await _fail(guide_id, _claude_error("Baustein-Fehler", returncode, bs_out, bs_err)) + return + if not bausteine_path.exists(): + await _fail(guide_id, "Baustein-Datei wurde nicht erstellt") + return + bausteine = bausteine_path.read_text(encoding="utf-8") + + # Step 2: Generator-Agent erstellt HTML nach Bausteinen + await _set_progress(guide_id, "Generiere HTML…") + current_step = "Generierung" + gen_prompt = _build_guide_prompt(topic, format_name, html_path, bausteine, instructions, project) + returncode, stdout, stderr = await run_agent(guide_id, gen_prompt, AGENT_TIMEOUT, provider=provider, role="guide", capabilities="full") + + if guide_id in _cancelled: + return + if returncode != 0: + await _fail(guide_id, _claude_error("Generator-Fehler", returncode, stdout, stderr)) + return + + if not html_path.exists(): + await _fail(guide_id, "HTML-Datei wurde nicht erstellt") + return + + now = datetime.now(timezone.utc).isoformat() + await update_guide( + guide_id, status="done", progress=None, updated_at=now, + ) + + except asyncio.TimeoutError: + await _fail(guide_id, f"Timeout bei {current_step} nach {AGENT_TIMEOUT}s") + except Exception as e: + await _fail(guide_id, str(e)[:2000]) + finally: + _cancelled.discard(guide_id) + + +def _claude_error(label: str, returncode: int, stdout: str, stderr: str) -> str: + stderr = (stderr or "").strip() + if stderr: + return f"{label}: {stderr[:1000]}" + tail = (stdout or "").strip()[-500:] + if tail: + return f"{label} (exit {returncode}, stderr leer): …{tail}" + return f"{label} (exit {returncode}, ohne Ausgabe)" + + +async def _fail(guide_id: str, msg: str) -> None: + now = datetime.now(timezone.utc).isoformat() + await update_guide(guide_id, status="error", progress=None, error_msg=msg, updated_at=now) + + +def _build_guide_chat_prompt(topic: str, format_name: str, section: str, outline: str, messages: list[dict]) -> str: + transcript = "\n".join( + f"{'Nutzer' if m.get('role') == 'user' else 'Assistent'}: {m.get('content', '')}" + for m in messages + ) + return _prompt( + "Chat", + topic=topic, format_name=format_name, + outline_block=outline.strip() or "(keine)", + section_block=section.strip() or "(kein Abschnitt erkannt)", + transcript=transcript, + ) + + +async def chat_with_guide(topic: str, format_name: str, section: str, outline: str, messages: list[dict], provider: str = DEFAULT_PROVIDER) -> str: + try: + prompt = _build_guide_chat_prompt(topic, format_name, section, outline, messages) + returncode, stdout, stderr = await run_agent( + "chat-" + str(uuid.uuid4()), prompt, 240, provider=provider, role="fast", capabilities="none" + ) + if returncode != 0: + return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut." + reply = stdout.strip() + return reply or "Entschuldigung, ich habe keine Antwort erhalten." + except Exception: + return "Entschuldigung, das hat nicht geklappt. Bitte versuche es erneut." diff --git a/backend/main.py b/backend/main.py index b4847a3..ef17940 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,13 +3,14 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.staticfiles import StaticFiles -from config import FRONTEND_DIST +from config import FRONTEND_DIST, STORAGE_DIR from database import init_db, close_db from routes import router @asynccontextmanager async def lifespan(app: FastAPI): + (STORAGE_DIR / "html").mkdir(parents=True, exist_ok=True) await init_db() yield await close_db() diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..3aa8421 --- /dev/null +++ b/backend/models.py @@ -0,0 +1,62 @@ +from pydantic import BaseModel, Field +from typing import Literal + +FormatType = Literal[ + "OnePager", + "MiniGuide", + "Guide", +] + +ProviderType = Literal["claude", "minimax"] + + +class GuideCreateRequest(BaseModel): + topic: str = Field(min_length=1, max_length=100) + format: FormatType + instructions: str = Field(default="", max_length=2000) + provider: ProviderType = "claude" + + +class ProjectResponse(BaseModel): + name: str + + +class ProviderInfo(BaseModel): + id: str + available: bool + + +class GuideResponse(BaseModel): + id: str + topic: str + format: str + status: str + progress: str | None = None + error_msg: str | None = None + created_at: str + updated_at: str + + +class ChatMessage(BaseModel): + role: Literal["user", "assistant"] + content: str = Field(min_length=1, max_length=8000) + + +class GuideChatRequest(BaseModel): + section: str = Field(default="", max_length=20000) + outline: str = Field(default="", max_length=8000) + messages: list[ChatMessage] = Field(min_length=1) + provider: ProviderType = "claude" + + +class GuideChatResponse(BaseModel): + reply: str + + +class ProgressUpdate(BaseModel): + chapter: str = Field(min_length=1, max_length=100) + done: bool + + +class ProgressResponse(BaseModel): + chapters: list[str] diff --git a/backend/paths.py b/backend/paths.py new file mode 100644 index 0000000..b22125b --- /dev/null +++ b/backend/paths.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from config import STORAGE_DIR, PROJECTS_DIR + + +def safe_basename(topic: str, format_name: str) -> str: + clean = topic.replace("/", "_").replace("\x00", "") + return f"{clean} - {format_name}" + + +def final_html_path(topic: str, format_name: str) -> Path: + return STORAGE_DIR / "html" / f"{safe_basename(topic, format_name)}.html" + + +def project_dir(name: str) -> Path: + return PROJECTS_DIR / name diff --git a/backend/routes.py b/backend/routes.py index e067e7c..da0578f 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -1,8 +1,151 @@ -from fastapi import APIRouter +import asyncio +import shutil +import uuid +from datetime import datetime, timezone + +from fastapi import APIRouter, HTTPException +from fastapi.responses import FileResponse + +from agents import provider_available +from config import FORMAT_META, PROJECTS_DIR, PROVIDERS +from database import ( + create_guide, delete_guide, get_guide, list_guides, + list_progress, set_progress, delete_progress, +) +from generator import generate_guide, cancel_guide, chat_with_guide +from models import ( + GuideCreateRequest, GuideResponse, + GuideChatRequest, GuideChatResponse, + ProgressUpdate, ProgressResponse, ProjectResponse, ProviderInfo, +) +from paths import final_html_path, project_dir router = APIRouter(prefix="/api") -@router.get("/health") -async def health(): +@router.get("/formats") +async def get_formats(): + return FORMAT_META + + +@router.get("/providers", response_model=list[ProviderInfo]) +async def get_providers(): + return [{"id": pid, "available": provider_available(pid)} for pid in PROVIDERS] + + +def _safe_project_name(name: str) -> str: + if not name or "/" in name or "\\" in name or ".." in name or "\x00" in name: + raise HTTPException(400, "Ungültiger Projektname") + return name + + +@router.get("/projects", response_model=list[ProjectResponse]) +async def list_projects(): + if not PROJECTS_DIR.is_dir(): + return [] + return [{"name": entry.name} for entry in sorted(PROJECTS_DIR.iterdir()) if entry.is_dir()] + + +@router.delete("/projects/{name}") +async def remove_project(name: str): + _safe_project_name(name) + pdir = project_dir(name) + if not pdir.is_dir(): + raise HTTPException(404, "Projekt nicht gefunden") + shutil.rmtree(pdir) return {"ok": True} + + +@router.post("/guides", response_model=GuideResponse) +async def create(req: GuideCreateRequest): + now = datetime.now(timezone.utc).isoformat() + guide = { + "id": str(uuid.uuid4()), + "topic": req.topic.strip(), + "format": req.format, + "instructions": req.instructions.strip(), + "status": "queued", + "progress": None, + "created_at": now, + "updated_at": now, + } + await create_guide(guide) + asyncio.create_task(generate_guide(guide["id"], guide["topic"], guide["format"], guide["instructions"], req.provider)) + return guide + + +@router.get("/guides", response_model=list[GuideResponse]) +async def list_all(): + return await list_guides() + + +@router.get("/guides/{guide_id}", response_model=GuideResponse) +async def get_one(guide_id: str): + guide = await get_guide(guide_id) + if guide is None: + raise HTTPException(404, "Guide nicht gefunden") + return guide + + +@router.get("/guides/{guide_id}/html") +async def download_html(guide_id: str): + guide = await get_guide(guide_id) + if guide is None: + raise HTTPException(404, "Guide nicht gefunden") + if guide["status"] != "done": + raise HTTPException(404, "HTML nicht verfügbar") + html_path = final_html_path(guide["topic"], guide["format"]) + if not html_path.exists(): + raise HTTPException(404, "Datei nicht gefunden") + return FileResponse(html_path, media_type="text/html", content_disposition_type="inline") + + +@router.post("/guides/{guide_id}/chat", response_model=GuideChatResponse) +async def guide_chat(guide_id: str, req: GuideChatRequest): + guide = await get_guide(guide_id) + if guide is None: + raise HTTPException(404, "Guide nicht gefunden") + reply = await chat_with_guide( + guide["topic"], guide["format"], req.section, req.outline, + [m.model_dump() for m in req.messages], + provider=req.provider, + ) + return {"reply": reply} + + +@router.post("/guides/{guide_id}/cancel") +async def cancel(guide_id: str): + cancelled = await cancel_guide(guide_id) + if not cancelled: + raise HTTPException(404, "Kein aktiver Prozess gefunden") + return {"ok": True} + + +@router.delete("/guides/{guide_id}") +async def remove(guide_id: str): + guide = await get_guide(guide_id) + if guide is None: + raise HTTPException(404, "Guide nicht gefunden") + html_path = final_html_path(guide["topic"], guide["format"]) + html_path.unlink(missing_ok=True) + html_path.with_suffix(".bausteine.md").unlink(missing_ok=True) + await delete_progress(guide_id) + await delete_guide(guide_id) + return {"ok": True} + + +@router.get("/guides/{guide_id}/progress", response_model=ProgressResponse) +async def get_progress(guide_id: str): + guide = await get_guide(guide_id) + if guide is None: + raise HTTPException(404, "Guide nicht gefunden") + return {"chapters": await list_progress(guide_id)} + + +@router.post("/guides/{guide_id}/progress", response_model=ProgressResponse) +async def update_progress(guide_id: str, req: ProgressUpdate): + guide = await get_guide(guide_id) + if guide is None: + raise HTTPException(404, "Guide nicht gefunden") + await set_progress(guide_id, req.chapter, req.done) + return {"chapters": await list_progress(guide_id)} diff --git a/dev-ops/opencode.json b/dev-ops/opencode.json new file mode 100644 index 0000000..5e4f819 --- /dev/null +++ b/dev-ops/opencode.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "minimax": { + "options": { + "apiKey": "{env:MINIMAX_API_KEY}" + }, + "models": { + "MiniMax-M3": { + "name": "MiniMax M3" + } + } + } + }, + "mcp": { + "minimax-search": { + "type": "local", + "command": ["uvx", "minimax-coding-plan-mcp"], + "environment": { + "MINIMAX_API_KEY": "{env:MINIMAX_API_KEY}", + "MINIMAX_API_HOST": "https://api.minimax.io" + } + } + }, + "agent": { + "full": { + "description": "Alle Tools: Dateien, Bash, Websuche", + "permission": { + "edit": "allow", + "bash": "allow", + "webfetch": "allow" + } + }, + "files": { + "description": "Dateien lesen/schreiben + Bash, keine Websuche", + "permission": { + "edit": "allow", + "bash": "allow", + "webfetch": "deny" + }, + "tools": { + "minimax-search*": false + } + }, + "readonly": { + "description": "Nur Dateien lesen", + "permission": { + "edit": "deny", + "bash": "deny", + "webfetch": "deny" + }, + "tools": { + "write": false, + "edit": false, + "bash": false, + "minimax-search*": false + } + }, + "text": { + "description": "Reine Textantwort, keine Tools", + "permission": { + "edit": "deny", + "bash": "deny", + "webfetch": "deny" + }, + "tools": { + "write": false, + "edit": false, + "bash": false, + "read": false, + "glob": false, + "grep": false, + "minimax-search*": false + } + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 47e2c21..5641bd6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,10 +4,15 @@ services: context: . container_name: creator restart: unless-stopped + environment: + - CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN:-} + - MINIMAX_API_KEY=${MINIMAX_API_KEY:-} networks: - web volumes: - - ./creator.db:/app/creator.db + - ./storage:/app/storage + - ./projects:/app/projects + - ./.claude-data:/home/app/.claude labels: - "traefik.enable=true" - "traefik.http.routers.creatorapp.rule=Host(`creator.marha.de`)" diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..cd68f14 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,39 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo + +.eslintcache + +# Cypress +/cypress/videos/ +/cypress/screenshots/ + +# Vitest +__screenshots__/ + +# Vite +*.timestamp-*-*.mjs diff --git a/frontend/.vscode/extensions.json b/frontend/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/frontend/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..347b0ce --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,38 @@ +# frontend + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Recommended Browser Setup + +- Chromium-based browsers (Chrome, Edge, Brave, etc.): + - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) + - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters) +- Firefox: + - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/) + - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/) + +## Customize configuration + +See [Vite Configuration Reference](https://vite.dev/config/). + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Compile and Minify for Production + +```sh +npm run build +``` diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c44e30d..15b48b7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,8 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "dompurify": "^3.4.7", + "marked": "^18.0.4", "vue": "^3.5.32" }, "devDependencies": { @@ -20,13 +22,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -35,9 +37,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", "dev": true, "license": "MIT", "engines": { @@ -45,21 +47,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -76,14 +78,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -93,27 +95,27 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", - "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -123,18 +125,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", - "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz", + "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.29.7", - "@babel/helper-member-expression-to-functions": "^7.29.7", - "@babel/helper-optimise-call-expression": "^7.29.7", - "@babel/helper-replace-supers": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", - "@babel/traverse": "^7.29.7", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.29.0", "semver": "^6.3.1" }, "engines": { @@ -145,9 +147,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, "license": "MIT", "engines": { @@ -155,43 +157,43 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", - "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -201,22 +203,22 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", - "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -224,15 +226,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", - "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.29.7", - "@babel/helper-optimise-call-expression": "^7.29.7", - "@babel/traverse": "^7.29.7" + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -242,41 +244,41 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", - "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -284,26 +286,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -313,15 +315,15 @@ } }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz", - "integrity": "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz", + "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/plugin-syntax-decorators": "^7.29.7" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-decorators": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -331,13 +333,13 @@ } }, "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz", - "integrity": "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", + "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -347,13 +349,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", - "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -376,13 +378,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", - "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -392,13 +394,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", - "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -408,17 +410,17 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", - "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.29.7", - "@babel/helper-create-class-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", - "@babel/plugin-syntax-typescript": "^7.29.7" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -428,33 +430,33 @@ } }, "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -462,13 +464,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -577,9 +579,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", "dev": true, "license": "MIT", "funding": { @@ -594,9 +596,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", "cpu": [ "arm64" ], @@ -611,9 +613,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", "cpu": [ "arm64" ], @@ -628,9 +630,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", "cpu": [ "x64" ], @@ -645,9 +647,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", "cpu": [ "x64" ], @@ -662,9 +664,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", "cpu": [ "arm" ], @@ -679,9 +681,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", "cpu": [ "arm64" ], @@ -696,9 +698,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", "cpu": [ "arm64" ], @@ -713,9 +715,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", "cpu": [ "ppc64" ], @@ -730,9 +732,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", "cpu": [ "s390x" ], @@ -747,9 +749,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", "cpu": [ "x64" ], @@ -764,9 +766,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", "cpu": [ "x64" ], @@ -781,9 +783,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", "cpu": [ "arm64" ], @@ -798,9 +800,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", "cpu": [ "wasm32" ], @@ -817,9 +819,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", "cpu": [ "arm64" ], @@ -834,9 +836,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", "cpu": [ "x64" ], @@ -868,6 +870,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@vitejs/plugin-vue": { "version": "6.0.7", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", @@ -939,53 +948,53 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz", - "integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.3", - "@vue/shared": "3.5.35", + "@vue/shared": "3.5.34", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz", - "integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.35", - "@vue/shared": "3.5.35" + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz", - "integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.3", - "@vue/compiler-core": "3.5.35", - "@vue/compiler-dom": "3.5.35", - "@vue/compiler-ssr": "3.5.35", - "@vue/shared": "3.5.35", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.15", + "postcss": "^8.5.14", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz", - "integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.35", - "@vue/shared": "3.5.35" + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" } }, "node_modules/@vue/devtools-core": { @@ -1023,59 +1032,59 @@ "license": "MIT" }, "node_modules/@vue/reactivity": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz", - "integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.35" + "@vue/shared": "3.5.34" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz", - "integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.35", - "@vue/shared": "3.5.35" + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz", - "integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.35", - "@vue/runtime-core": "3.5.35", - "@vue/shared": "3.5.35", + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz", - "integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.35", - "@vue/shared": "3.5.35" + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" }, "peerDependencies": { - "vue": "3.5.35" + "vue": "3.5.34" } }, "node_modules/@vue/shared": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz", - "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", "license": "MIT" }, "node_modules/ansis": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.1.tgz", - "integrity": "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.0.tgz", + "integrity": "sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg==", "dev": true, "license": "ISC", "engines": { @@ -1083,9 +1092,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.34", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz", - "integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==", + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1260,10 +1269,19 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.7.tgz", + "integrity": "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.368", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", - "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==", + "version": "1.5.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", + "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", "dev": true, "license": "ISC" }, @@ -1371,19 +1389,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-in-ssh": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", - "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -1739,6 +1744,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.4.tgz", + "integrity": "sha512-c/BTaKzg0G6ezQx97DAkYU7k0HM6ys0FqYeKBL6hlBByZwy+ycA1+f0vDdjMHKKeEjdgkx0GOv9Il6D+85cOqA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -1775,29 +1792,15 @@ } }, "node_modules/node-releases": { - "version": "2.0.47", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", - "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, - "node_modules/obug": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", - "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", @@ -1806,21 +1809,19 @@ "license": "MIT" }, "node_modules/open": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", - "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "license": "MIT", "dependencies": { - "default-browser": "^5.4.0", + "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", - "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", - "powershell-utils": "^0.1.0", - "wsl-utils": "^0.3.0" + "wsl-utils": "^0.1.0" }, "engines": { - "node": ">=20" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1887,27 +1888,14 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/powershell-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", - "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", + "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -1917,21 +1905,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" } }, "node_modules/run-applescript": { @@ -1982,9 +1970,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -2065,17 +2053,17 @@ } }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", - "rolldown": "1.0.3", - "tinyglobby": "^0.2.17" + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" }, "bin": { "vite": "bin/vite.js" @@ -2142,33 +2130,6 @@ } } }, - "node_modules/vite-dev-rpc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vite-dev-rpc/-/vite-dev-rpc-2.0.0.tgz", - "integrity": "sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA==", - "dev": true, - "license": "MIT", - "dependencies": { - "birpc": "^4.0.0", - "vite-hot-client": "^2.2.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 || ^8.0.0" - } - }, - "node_modules/vite-dev-rpc/node_modules/birpc": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-4.0.0.tgz", - "integrity": "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/vite-hot-client": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.2.0.tgz", @@ -2182,38 +2143,6 @@ "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0" } }, - "node_modules/vite-plugin-inspect": { - "version": "11.4.1", - "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-11.4.1.tgz", - "integrity": "sha512-ShOFe2PURXGvRS5OrgmOLZOCwDTD7dEBVt0tMpFPKb9AsvqXKCRGM8QgKrUbRbJYFXScHvDPpGRd28rYidC0tA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansis": "^4.3.0", - "error-stack-parser-es": "^1.0.5", - "obug": "^2.1.1", - "ohash": "^2.0.11", - "open": "^11.0.0", - "perfect-debounce": "^2.1.0", - "sirv": "^3.0.2", - "unplugin-utils": "^0.3.1", - "vite-dev-rpc": "^2.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vite": "^6.0.0 || ^7.0.0-0 || ^8.0.0-0" - }, - "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - } - } - }, "node_modules/vite-plugin-vue-devtools": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.1.2.tgz", @@ -2235,6 +2164,55 @@ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/vite-plugin-vue-devtools/node_modules/vite-plugin-inspect": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz", + "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.1.0", + "debug": "^4.4.1", + "error-stack-parser-es": "^1.0.5", + "ohash": "^2.0.11", + "open": "^10.2.0", + "perfect-debounce": "^2.0.0", + "sirv": "^3.0.1", + "unplugin-utils": "^0.3.0", + "vite-dev-rpc": "^1.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/vite-plugin-inspect/node_modules/vite-dev-rpc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz", + "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==", + "dev": true, + "license": "MIT", + "dependencies": { + "birpc": "^2.4.0", + "vite-hot-client": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" + } + }, "node_modules/vite-plugin-vue-inspector": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-6.0.0.tgz", @@ -2257,16 +2235,16 @@ } }, "node_modules/vue": { - "version": "3.5.35", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz", - "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.35", - "@vue/compiler-sfc": "3.5.35", - "@vue/runtime-dom": "3.5.35", - "@vue/server-renderer": "3.5.35", - "@vue/shared": "3.5.35" + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" }, "peerDependencies": { "typescript": "*" @@ -2278,17 +2256,16 @@ } }, "node_modules/wsl-utils": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", - "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", "dev": true, "license": "MIT", "dependencies": { - "is-wsl": "^3.1.0", - "powershell-utils": "^0.1.0" + "is-wsl": "^3.1.0" }, "engines": { - "node": ">=20" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/frontend/package.json b/frontend/package.json index ed0a264..7a89248 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,8 @@ "preview": "vite preview" }, "dependencies": { + "dompurify": "^3.4.7", + "marked": "^18.0.4", "vue": "^3.5.32" }, "devDependencies": { diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a30e4b9..cdcbf19 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,13 +1,42 @@ diff --git a/frontend/src/api.js b/frontend/src/api.js index 3e50df1..3b8350f 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1,6 +1,64 @@ const BASE = '/api' -export async function fetchHealth() { - const res = await fetch(`${BASE}/health`) +export async function fetchGuides() { + const res = await fetch(`${BASE}/guides`) + return res.json() +} + +export async function createGuide(topic, format, instructions = '', provider = 'claude') { + const res = await fetch(`${BASE}/guides`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ topic, format, instructions, provider }), + }) + return res.json() +} + +export async function fetchProviders() { + const res = await fetch(`${BASE}/providers`) + return res.json() +} + +export async function fetchProjects() { + const res = await fetch(`${BASE}/projects`) + return res.json() +} + +export async function deleteProject(name) { + await fetch(`${BASE}/projects/${encodeURIComponent(name)}`, { method: 'DELETE' }) +} + +export async function cancelGuide(id) { + await fetch(`${BASE}/guides/${id}/cancel`, { method: 'POST' }) +} + +export async function deleteGuide(id) { + await fetch(`${BASE}/guides/${id}`, { method: 'DELETE' }) +} + +export function htmlUrl(id) { + return `${BASE}/guides/${id}/html` +} + +export async function fetchProgress(id) { + const res = await fetch(`${BASE}/guides/${id}/progress`) + return res.json() +} + +export async function setProgress(id, chapter, done) { + const res = await fetch(`${BASE}/guides/${id}/progress`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ chapter, done }), + }) + return res.json() +} + +export async function chatGuide(id, { section, outline, messages, provider = 'claude' }) { + const res = await fetch(`${BASE}/guides/${id}/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ section, outline, messages, provider }), + }) return res.json() } diff --git a/frontend/src/components/TopicDetail.vue b/frontend/src/components/TopicDetail.vue new file mode 100644 index 0000000..95fa0d2 --- /dev/null +++ b/frontend/src/components/TopicDetail.vue @@ -0,0 +1,637 @@ + + + + + diff --git a/frontend/src/components/TopicSidebar.vue b/frontend/src/components/TopicSidebar.vue new file mode 100644 index 0000000..974cab7 --- /dev/null +++ b/frontend/src/components/TopicSidebar.vue @@ -0,0 +1,574 @@ + + + + + diff --git a/templates/Format/Guide.md b/templates/Format/Guide.md new file mode 100644 index 0000000..13f23e2 --- /dev/null +++ b/templates/Format/Guide.md @@ -0,0 +1,782 @@ +# Guide Style System — Authoring & Build Specification + +This document is a **complete, self-contained specification** for producing a polished, book-style guide as a single HTML file that renders to a clean A4 PDF. It is **topic-neutral**: use it for programming subjects (PHP, Godot, Blender) and equally for non-technical ones (nutrition, finance, psychology, communication, language learning, …). + +You will normally be given two things: this specification and possibly one reference HTML file built with it. From those alone you must be able to: + +1. Gather the subject knowledge yourself (research as needed). +2. Decide a structure (parts → chapters). +3. Write a single HTML file that embeds the CSS from this document verbatim. +4. Convert that HTML to PDF. + +Follow this spec exactly. The visual identity depends on small details (spacing, weights, the single accent color), so do not improvise the CSS. You **do** have full freedom over content, structure, length, and which optional building blocks you use. + +--- + +## 1. Output contract + +- **One HTML file**, self-contained: a single ` + + + + +
+ + +
+ + +
+
+
+ + + +
+
+ + + + +``` + +You may generate this HTML however you like — by hand, or with a small script that concatenates strings. A script helps for long guides because it keeps each chapter in its own readable chunk. If you use a script, **the script is throwaway**; the deliverable is the HTML (and the PDF), not the script. + +--- + +## 6. The building blocks + +Below is the exact markup for every block. Copy these shapes; fill in content. + +### 6.1 Cover + +```html +
+ +

MAIN TITLESubtitle line

+
One or two sentences describing what the guide covers and who it is for.
+
+ N Parts · M Chapters + Edition / version · Year + Focus: … +
+
+
+``` + +- **`.cover-logo`** — 1–4 characters or a single symbol that evokes the topic. Examples: `php`, `gd` (Godot), a Blender-style `b`, `€` (finance), `Ψ` (psychology), `EN` (English), `🍎` is **not** allowed (no emoji in the logo — keep it crisp). Prefer short letterforms or a geometric glyph (`◆ ● ▲ ■`). +- **`.cover h1`** — the big title. The `` is an optional lighter, smaller second line (e.g. `PHP` then `The Complete Guide`). Drop the span for a single-line title. +- **`.cover-deco`** — a giant, very faint background glyph in the bottom-right. Pick something topic-flavored: a code fragment (``s. Use it to state scope (parts/chapters), edition/year, and the focus. This is where you set the reader's expectations about length and depth. + +### 6.2 Table of contents + +List every part, and under it every chapter, numbered sequentially across the whole guide. There are **no real page numbers** (WeasyPrint does not back-fill them here), so the `.toc-page` element is optional — omit it, or use it only if you maintain numbers yourself. The dotted leader still looks right without a trailing number. + +```html +
+

Contents

+ +
Part 1 · Fundamentals
+
+ 1 + First chapter title + +
+
+ 2 + Second chapter title + +
+ +
Part 2 · Going Deeper
+
+ 3 + + +
+
+``` + +### 6.3 Part divider + +One full page that introduces a part. The chapter list mirrors the TOC entries for that part. + +```html +
+
Part 1
+

Fundamentals

+
Italic one-liner describing the arc of this part.
+
+ 1 · First chapter + 2 · Second chapter + 3 · Third chapter +
+
+``` + +### 6.4 Chapter + +The `.chapter-head` is special: its text is captured into the **running header** at the top-right of every page in that chapter (via `string-set: chaptertitle content()`). Note that `content()` concatenates the text of **all** children, so the chapter-number span and the title run together in the header (e.g. "Chapter 1Title"). For a clean separator, end the `.chapter-num` text with a trailing separator such as `Chapter 1 · ` (trailing space + middot) — or accept the run-on; both are acceptable. Either way keep the chapter `

` reasonably short so it fits on one header line. + +```html +
+
+ Chapter 1 +

Chapter title

+
+ +

Framing sentence(s) — why this matters.

+ +

A section

+

Body text. Use bold for the key term in a sentence. Inline monospace + like term works for any short literal — + a command, a key name, a nutrient, a chord, a German case.

+ +

A sub-section

+

+ + +
+``` + +### 6.5 Callouts + +Four flavors. Each has a short uppercase label as the **first ``** inside `.callout-body`, then body text. The icon column holds one glyph. + +```html +
+
+
LABELBody text giving a practical tip.
+
+ +
+
!
+
LABELBody text warning about a trap.
+
+ +
+
i
+
LABELBody text for a neutral side note.
+
+ +
+
+
LABELBody text for an optional deeper dive.
+
+``` + +Meaning of each flavor (topic-independent): + +| Flavor | Color | Use for | +|---|---|---| +| `tip` | green, ✓ | best practice, a recommendation, a shortcut | +| `warn` | red, ! | a common mistake, risk, or thing to avoid | +| `note` | accent, i | a neutral aside, clarification, context | +| `deep` | gold, ◆ | optional depth: history, edge case, advanced detail | + +Standard icon entities: tip `✓` (✓), warn `!`, note `i`, deep `◆` (◆). You may substitute a more fitting single glyph, but keep it one character. + +### 6.6 Tables + +Plain `` with a header row. The styling (accent header, zebra rows) is automatic. Keep tables to a few columns so they fit A4 width. + +```html +
+ + + +
Column AColumn B
valuevalue
valuevalue
+``` + +Tables are one of the most useful blocks for **non-technical** topics too: nutrient comparisons, vocabulary lists, pros/cons, decision matrices, dosage/timing, term glossaries. + +--- + +## 7. Code blocks — optional, technical topics only + +The dark `
` block and the highlight span classes exist for subjects that genuinely involve code or other monospaced literal text (config, shell commands, formulas). **For non-technical guides, do not use `
` blocks at all** — they would look out of place. Use tables, lists, callouts, and worked examples instead. (`code.inline` is fine everywhere for short literals.)
+
+When you do use code, you **hand-write the highlighting** by wrapping tokens in spans. The classes are intentionally generic so they map onto any language:
+
+| Class | Generic meaning | Typical use |
+|---|---|---|
+| `.c` | de-emphasized | comments |
+| `.k` | keyword / control word | `if`, `function`, `class`, `return` |
+| `.s` | literal value | strings, numbers |
+| `.f` | callable name | function / method names |
+| `.t` | type / tag / class | type names, HTML tags, class names |
+| `.v` | identifier | variables |
+| `.a` | annotation | attributes, decorators, directives |
+
+Rules for code blocks:
+
+- Keep snippets **short** (roughly 2–12 lines). They must fit on one page — `page-break-inside: avoid` is set, so an oversized block can overflow. Split long examples into several captioned blocks.
+- **Escape HTML inside code**: write `<`, `>`, `&`. This is the most common rendering bug.
+- Inside the dark block, a comment uses ``. Indentation is literal spaces (the block is `white-space: pre`).
+- You do not need to highlight every token — highlight the ones that aid reading (keywords, strings, names). Plain text inside `
` is fine and renders in the default light color.
+
+Example (PHP-flavored, but the pattern is language-agnostic):
+
+```html
+
function greet(string $name): string {
+    return "Hello, $name";   // interpolation
+}
+``` + +--- + +## 8. Pitfalls — read before building (these caused real bugs) + +1. **Escape `<`, `>`, `&` inside `
` and ``.** Unescaped angle brackets silently swallow content or break layout. Always `< > &`.
+2. **Typographic quotes in prose, straight quotes in attributes.** In body text use the target language's real quotation marks (German `„ … "`, English `" … "`). Never let a quote character inside running text collide with HTML attribute quotes. If you generate the HTML from a script, be careful that closing typographic quotes are not accidentally written as escaped ASCII quotes — that corrupts both the string and any nearby `class="…"`. The safe approach: type real `„ … "` / `" … "` glyphs in prose, and reserve `"` strictly for HTML attributes.
+3. **`--footer-label` must be a quoted CSS string**, e.g. `--footer-label: "Nutrition Guide";`. An unquoted value breaks the `@page` rule.
+4. **Keep code blocks and callouts short enough to fit one page.** `page-break-inside: avoid` prevents splitting but cannot shrink an oversized block; it will overflow the page. Break large blocks up.
+5. **Multibyte text is fine** (umlauts, accents, CJK, symbols) — the fonts and UTF-8 charset handle it. But if you ever measure string length in a generator script, count characters, not bytes.
+6. **Chapter `

` feeds the running header.** Keep it short; a very long chapter title wraps awkwardly in the 8pt header. +7. **Don't add web fonts or external assets.** The look depends on the system-font stack already specified. Adding fonts changes metrics and spacing. +8. **One accent, three shades.** Don't introduce extra brand colors. Variety comes from the callout colors (green/red/gold), which are fixed and meaningful — not decorative. +9. **Tables and code don't split** (`page-break-inside: avoid`). If a table is very long, either let it be its own short section or split it into two. + +--- + +## 9. Build & verify + +1. Write `guide.html` (single file, embedded ` + + + +
+ +

PHPDer Komplett-Guide

+
Von den ersten Zeilen bis zu Architektur, Patterns und Experten-Nischen. Modernes PHP 8.5 – gründlich erklärt, mit kurzem, lauffähigem Code.
+
+ 8 Teile · 46 Kapitel + PHP 8.5 · Stand 2026 + Schwerpunkt: Sprache & OOP +
+
<?php
+

Inhalt

+
Teil 1 · Grundlagen
+
1PHP einrichten & ausführen
+
2Variablen & Datentypen
+
3Operatoren & Ausdrücke
+
4Strings im Detail
+
5Bedingungen & Verzweigungen
+
6Schleifen
+
7Arrays
+
8Funktionen
+
Teil 2 · Struktur & Werkzeug
+
9Code aufteilen: include & require
+
10Fehler & Exceptions
+
11Datum & Zeit
+
12Dateien lesen & schreiben
+
13JSON & Datenformate
+
14Composer & Autoloading
+
Teil 3 · Typsystem & moderne Features
+
15Typen & strict_types
+
16Union-, Nullable- & spezielle Typen
+
17Enums
+
18Moderne Syntax-Schmankerl
+
Teil 4 · Objektorientierung
+
19Klassen & Objekte
+
20Sichtbarkeit & Kapselung
+
21Konstruktoren modern
+
22Vererbung
+
23Abstrakte Klassen & Interfaces
+
24Traits
+
25Statisches & Konstanten
+
26Magische Methoden
+
Teil 5 · Fortgeschrittene Sprache
+
27Generics-Denken & Collections
+
28Iteratoren & Generatoren
+
29Closures & Bindung
+
30Attribute
+
31Reflection
+
32Namespaces im Detail
+
Teil 6 · Architektur & Patterns
+
33SOLID-Prinzipien
+
34Dependency Injection
+
35Häufige Entwurfsmuster
+
36Wertobjekte & DTOs
+
37Fehlerbehandlung als Architektur
+
Teil 7 · Qualität & Profi-Werkzeug
+
38Testen mit PHPUnit
+
39Statische Analyse
+
40Code-Style & Tooling
+
41Debugging & Xdebug
+
Teil 8 · Experten & Nischen
+
42Performance & OPcache
+
43Speicher & Referenzen
+
44Prozesse, FFI & Fibers
+
45CLI-Programme bauen
+
46Sicherheit: die Klassiker
+
+
+
Teil 1
+

Grundlagen

+
Vom ersten echo bis zu Arrays, Schleifen und eigenen Funktionen. Alles, was du brauchst, um echte kleine Programme zu schreiben.
+
1 · PHP einrichten & ausführen2 · Variablen & Datentypen3 · Operatoren & Ausdrücke4 · Strings im Detail5 · Bedingungen & Verzweigungen6 · Schleifen7 · Arrays8 · Funktionen
+
+
+
+ Kapitel 1 +

PHP einrichten & ausführen

+
+ +

PHP ist eine Skriptsprache, die Code in Ausgaben verwandelt – Text, HTML, JSON. Sie läuft auf deinem Rechner und auf Webservern. Bevor wir programmieren, brauchst du eine lauffähige Installation.

+ +

Installation

+

PHP ist auf jedem Betriebssystem mit wenigen Befehlen installiert. Die aktuelle stabile Version ist PHP 8.5 (Stand 2026); für neue Projekte ist das die richtige Wahl, PHP 8.4 der konservative Fallback.

+ + + + + + +
SystemBefehl
macOS (Homebrew)brew install php
Ubuntu / Debiansudo apt install php-cli
Fedorasudo dnf install php-cli
WindowsWSL2 + Ubuntu, dann apt install php-cli
+

Prüfe danach im Terminal, ob alles läuft:

+
php --version
+# PHP 8.5.6 (cli) ...
+ +

Die erste Datei

+

PHP-Code lebt in Dateien mit der Endung .php. Lege hallo.php an:

+
<?php
+
+echo "Hallo Welt!";
+

Die Zeile <?php öffnet einen PHP-Block. echo gibt Text aus. Der Text steht in Anführungszeichen und heißt String. Jede Anweisung endet mit einem Semikolon.

+

Ausführen:

+
php hallo.php
+# Hallo Welt!
+ +

Der eingebaute Webserver

+

PHP bringt einen Entwicklungs-Webserver mit. Damit testest du Web-Code ohne Apache oder Nginx:

+
php -S localhost:8000
+

Öffne http://localhost:8000 im Browser. Jede .php-Datei im Ordner wird nun ausgeführt und das Ergebnis ausgeliefert.

+ +

PHP eingebettet in HTML

+

PHP wurde fürs Web erfunden. Du kannst PHP-Blöcke direkt in HTML einstreuen. Alles außerhalb von <?php ... ?> wird unverändert ausgegeben:

+
<h1>Meine Seite</h1>
+<?php echo "Heute ist " . date("d.m.Y"); ?>
+
+
i
+
Reine PHP-DateienIn Dateien, die nur PHP enthalten (z. B. Klassen), lässt man das schließende ?> bewusst weg. Das verhindert versehentliche Leerzeichen in der Ausgabe – ein verbreiteter Standard (PSR-12).
+
+
+
+
+ Kapitel 2 +

Variablen & Datentypen

+
+ +

Eine Variable ist ein benannter Speicherplatz für einen Wert – wie eine beschriftete Box. In PHP beginnen Variablennamen immer mit einem Dollar-Zeichen.

+ +

Zuweisung

+
$name = "Marek";
+$alter = 34;
+$groesse = 1.82;
+$istAktiv = true;
+

Das einfache = bedeutet nicht „ist gleich", sondern „speichere den rechten Wert links". Das nennt man Zuweisung. Variablennamen sind frei wählbar, beginnen mit Buchstabe oder Unterstrich und sind case-sensitive: $name und $Name sind verschieden.

+ +

Die wichtigsten Datentypen

+ + + + + + + + +
TypBedeutungBeispiel
stringText"Hallo"
intGanzzahl42
floatKommazahl3.14
boolWahrheitswerttrue / false
arrayListe von Werten[1, 2, 3]
null„kein Wert"null
+ +

Typ herausfinden

+

PHP ist dynamisch typisiert: Eine Variable kann ihren Typ wechseln. Mit gettype() oder var_dump() siehst du, was drinsteckt:

+
$x = 42;
+var_dump($x);        // int(42)
+
+$x = "jetzt Text";
+var_dump($x);        // string(10) "jetzt Text"
+ +

Konstanten

+

Werte, die sich nie ändern, speicherst du in Konstanten. Sie haben kein $ und werden traditionell GROSS geschrieben:

+
const MWST = 0.19;
+echo MWST;        // 0.19
+
+
i
+
Das Dollar-ZeichenDas $ vor jeder Variable ist eine PHP-Besonderheit. Es erlaubt dem Interpreter, Variablen sofort von Schlüsselwörtern wie echo zu unterscheiden, und macht sie in eingebettetem HTML klar erkennbar.
+
+
+
+
+ Kapitel 3 +

Operatoren & Ausdrücke

+
+ +

Operatoren verknüpfen Werte zu neuen Werten. Sie sind das Handwerkszeug für jede Berechnung und jeden Vergleich.

+ +

Arithmetik

+
echo 7 + 3;     // 10
+echo 7 - 3;     // 4
+echo 7 * 3;     // 21
+echo 7 / 2;     // 3.5
+echo 7 % 3;     // 1  (Rest der Division)
+echo 2 ** 8;    // 256 (Potenz)
+ +

Zuweisungs-Kurzformen

+

Statt $x = $x + 5 schreibt man kürzer:

+
$x += 5;   // erhöhen um 5
+$x -= 2;   // verringern um 2
+$x *= 3;   // multiplizieren
+$x++;      // um 1 erhöhen
+$x--;      // um 1 verringern
+ +

Vergleiche

+ + + + + + + +
OperatorBedeutung
==gleich (Wert)
===gleich (Wert und Typ)
!= / !==ungleich / streng ungleich
< > <= >=kleiner, größer, …
<=>Spaceship: -1, 0 oder 1
+ +

Logik

+
$a = true;  $b = false;
+var_dump($a && $b);   // false (und)
+var_dump($a || $b);   // true  (oder)
+var_dump(!$a);        // false (nicht)
+
+
!
+
== gegen ===== vergleicht nur den Wert und wandelt Typen vorher um: 0 == "text" kann überraschende Ergebnisse liefern. === prüft Wert und Typ und ist fast immer die sichere Wahl. Gewöhne dir === als Standard an.
+
+
+
+
+ Kapitel 4 +

Strings im Detail

+
+ +

Text ist allgegenwärtig. PHP bietet viele Wege, Strings zu bauen, zu kombinieren und zu durchsuchen.

+ +

Anführungszeichen: einfach vs. doppelt

+

In doppelten Anführungszeichen werden Variablen direkt eingesetzt (Interpolation). In einfachen nicht:

+
$name = "Anna";
+echo "Hallo $name";   // Hallo Anna
+echo 'Hallo $name';   // Hallo $name
+

Bei zusammengesetzten Ausdrücken nutzt du geschweifte Klammern zur Abgrenzung:

+
echo "Summe: {$preis €}";
+ +

Verketten

+

Der Punkt-Operator klebt Strings zusammen:

+
$gruss = "Hallo, " . $name . "!";
+ +

Nützliche String-Funktionen

+ + + + + + + + + + +
FunktionZweck
strlen($s)Länge in Bytes
mb_strlen($s)Länge in Zeichen (Umlaute!)
strtoupper / strtolowerGroß-/Kleinschreibung
trim($s)Leerzeichen am Rand entfernen
str_replace(a, b, $s)ersetzen
str_contains($s, $t)enthält? (bool)
explode(",", $s)String → Array
implode(",", $arr)Array → String
+
$mail = "  Marek@example.com  ";
+echo strtolower(trim($mail));
+// marek@example.com
+ +

printf & sprintf

+

Für formatierte Ausgaben mit Platzhaltern:

+
printf("%s ist %d Jahre alt.\n", $name, $alter);
+$preis = sprintf("%.2f €", 3.5);  // "3.50 €"
+ +

Heredoc für lange Texte

+
$html = <<<HTML
+<h1>$name</h1>
+<p>Willkommen!</p>
+HTML;
+
+
+
mb_-Funktionen bei UmlautenBei Texten mit Umlauten oder Emojis nutze die mb_*-Varianten (Multibyte). strlen("Größe") zählt Bytes, mb_strlen("Größe") zählt Zeichen – das willst du fast immer.
+
+
+
+
+ Kapitel 5 +

Bedingungen & Verzweigungen

+
+ +

Programme entscheiden: „Wenn X, dann Y, sonst Z." Verzweigungen steuern, welcher Code unter welchen Umständen läuft.

+ +

if / elseif / else

+
$punkte = 75;
+
+if ($punkte >= 90) {
+    echo "Sehr gut";
+} elseif ($punkte >= 50) {
+    echo "Bestanden";
+} else {
+    echo "Durchgefallen";
+}
+

Die Bedingung in der Klammer muss einen Wahrheitswert ergeben. Der erste zutreffende Block läuft, der Rest wird übersprungen.

+ +

match – die moderne Alternative

+

Seit PHP 8 gibt es match: kompakt, typsicher (===) und es liefert einen Wert zurück:

+
$tag = 3;
+$name = match($tag) {
+    1, 2, 3, 4, 5 => "Werktag",
+    6, 7          => "Wochenende",
+    default      => "ungültig",
+};
+ +

Der ternäre Operator

+

Eine kurze if/else-Form für einfache Fälle:

+
$status = $alter >= 18 ? "erwachsen" : "minderjährig";
+ +

Null-Coalescing

+

Liefert den ersten Wert, der existiert und nicht null ist – praktisch für Standardwerte:

+
$name = $_GET['name'] ?? "Gast";
+
+
!
+
= statt == in BedingungenEin einzelnes = in einer Bedingung speichert einen Wert, statt ihn zu prüfen – und ergibt fast immer „wahr”. Das ist ein klassischer Anfängerfehler. In Bedingungen gehören mindestens zwei Gleichheitszeichen.
+
+
+
+
+ Kapitel 6 +

Schleifen

+
+ +

Schleifen wiederholen Code, bis eine Bedingung erfüllt ist. Sie ersparen dir, dieselbe Anweisung hundertmal zu tippen.

+ +

while – solange etwas gilt

+
$i = 1;
+while ($i <= 3) {
+    echo $i;     // 123
+    $i++;
+}
+ +

for – feste Anzahl

+

Drei Teile in einer Zeile: Start, Bedingung, Schritt.

+
for ($i = 0; $i < 5; $i++) {
+    echo $i;     // 01234
+}
+ +

foreach – über Listen

+

Die wichtigste Schleife in der Praxis: Sie geht jedes Element eines Arrays durch.

+
$obst = ["Apfel", "Birne", "Kirsche"];
+foreach ($obst as $frucht) {
+    echo $frucht . "\n";
+}
+

Mit Schlüssel und Wert zugleich:

+
foreach ($preise as $produkt => $preis) {
+    echo "$produkt: $preis €\n";
+}
+ +

break & continue

+

break bricht die Schleife ganz ab, continue springt zum nächsten Durchlauf:

+
foreach ($zahlen as $z) {
+    if ($z < 0) continue;   // negative überspringen
+    if ($z > 100) break;    // ab 100 aufhören
+    echo $z;
+}
+
+
+
foreach ist dein StandardIn der Praxis ist foreach die mit Abstand häufigste Schleife. for brauchst du nur, wenn du den Zähler selbst kontrollieren musst; while, wenn die Anzahl der Durchläufe vorher unbekannt ist.
+
+
+
+
+ Kapitel 7 +

Arrays

+
+ +

Ein Array speichert mehrere Werte unter einem Namen. In PHP ist es extrem flexibel: Liste, Schlüssel-Wert-Sammlung und Verschachtelung in einem.

+ +

Indizierte Arrays

+

Werte mit automatischen Nummern (beginnend bei 0):

+
$farben = ["rot", "grün", "blau"];
+echo $farben[0];      // rot
+$farben[] = "gelb";    // anhängen
+ +

Assoziative Arrays

+

Werte mit eigenen Schlüsseln – ideal für strukturierte Daten:

+
$person = [
+    "name"  => "Marek",
+    "alter" => 34,
+    "stadt" => "Kaltenkirchen",
+];
+echo $person["name"];   // Marek
+ +

Verschachtelung

+
$team = [
+    ["name" => "Anna", "rolle" => "Dev"],
+    ["name" => "Ben",  "rolle" => "Design"],
+];
+echo $team[0]["name"];   // Anna
+ +

Wichtige Array-Funktionen

+ + + + + + + + + +
FunktionZweck
count($a)Anzahl Elemente
in_array($x, $a)enthält Wert?
array_keys / array_valuesSchlüssel / Werte
sort / rsortauf-/absteigend sortieren
array_mapjeden Wert transformieren
array_filterWerte herausfiltern
array_mergeArrays zusammenfügen
+
$zahlen = [1, 2, 3, 4];
+$quadrate = array_map(fn($n) => $n ** 2, $zahlen);
+// [1, 4, 9, 16]
+$gerade = array_filter($zahlen, fn($n) => $n % 2 === 0);
+// [2, 4]
+
+
i
+
Ein Typ, viele RollenIn vielen Sprachen sind Liste und Wörterbuch getrennte Typen. PHP hat nur einen Array-Typ, der beides kann. Das ist praktisch, aber sei dir bewusst, ob deine Schlüssel Zahlen oder Strings sind – das beeinflusst Sortierung und Iteration.
+
+
+
+
+ Kapitel 8 +

Funktionen

+
+ +

Eine Funktion bündelt Code, den du benennen und wiederverwenden kannst. Sie nimmt Eingaben (Parameter) und liefert oft ein Ergebnis (Rückgabewert).

+ +

Definition & Aufruf

+
function begruessen($name) {
+    echo "Hallo, $name!\n";
+}
+begruessen("Marek");
+begruessen("Anna");
+ +

Rückgabewerte

+

return beendet die Funktion und liefert einen Wert an den Aufrufer zurück:

+
function addiere($a, $b) {
+    return $a + $b;
+}
+$summe = addiere(3, 5);   // 8
+

Unterschied: echo gibt etwas auf dem Bildschirm aus, return gibt einen Wert zurück, mit dem du weiterrechnen kannst.

+ +

Typdeklarationen

+

Modernes PHP gibt Parametern und Rückgabe feste Typen. Das macht Code sicherer und selbsterklärend:

+
function addiere(int $a, int $b): int {
+    return $a + $b;
+}
+ +

Standardwerte & benannte Argumente

+
function verbinde(string $host, int $port = 5432): string {
+    return "$host:$port";
+}
+verbinde("db.local");              // db.local:5432
+verbinde("db.local", port: 5433);  // benanntes Argument
+ +

Anonyme Funktionen & Arrow Functions

+

Funktionen ohne Namen, oft als Argument für andere Funktionen:

+
$verdopple = fn($x) => $x * 2;
+echo $verdopple(21);   // 42
+
+
+
Typen von Anfang anAuch wenn PHP Typen nicht erzwingt: Schreib sie hin. Typdeklarationen fangen Fehler früh ab, dokumentieren deine Absicht und sind die Grundlage für gute Editor-Unterstützung. Wir vertiefen das in Teil 3.
+
+
+
+
Teil 2
+

Struktur & Werkzeug

+
Code auf mehrere Dateien verteilen, Fehler sauber behandeln, mit Datum, Dateien und JSON arbeiten – und Composer als Paketmanager nutzen.
+
9 · Code aufteilen: include & require10 · Fehler & Exceptions11 · Datum & Zeit12 · Dateien lesen & schreiben13 · JSON & Datenformate14 · Composer & Autoloading
+
+
+
+ Kapitel 9 +

Code aufteilen: include & require

+
+ +

Sobald ein Programm wächst, willst du es auf mehrere Dateien verteilen – nach Themen geordnet. PHP bindet Dateien mit vier Schlüsselwörtern ein.

+ +

include und require

+

Beide fügen den Inhalt einer anderen Datei an dieser Stelle ein. Der Unterschied liegt im Fehlerfall:

+ + + + + +
BefehlWenn Datei fehlt
includeWarnung, Programm läuft weiter
requirefataler Fehler, Programm stoppt
include_once / require_oncebindet nur einmal ein
+
// funktionen.php
+function gruss($n) { return "Hi $n"; }
+
+// app.php
+require_once "funktionen.php";
+echo gruss("Marek");
+ +

Warum _once?

+

Bindest du dieselbe Datei zweimal ein, würde eine Funktion doppelt definiert – das ist ein Fehler. require_once merkt sich, was schon geladen wurde, und verhindert das.

+
+
i
+
In der Praxis: AutoloadingHeute bindet man Dateien selten von Hand ein. Composer (Kapitel 14) lädt Klassen automatisch, sobald sie gebraucht werden. require sieht man vor allem noch für den zentralen autoload.php-Einstieg.
+
+
+
+
+ Kapitel 10 +

Fehler & Exceptions

+
+ +

Fehler passieren: eine Datei fehlt, eine Eingabe ist ungültig, ein Server antwortet nicht. Exceptions sind PHPs strukturierter Weg, damit umzugehen.

+ +

try / catch

+

Du umschließt riskanten Code mit try. Tritt ein Fehler auf, springt PHP in den passenden catch-Block – das Programm stürzt nicht ab:

+
try {
+    $wert = 10 / $teiler;
+} catch (DivisionByZeroError $e) {
+    echo "Division durch Null!";
+}
+ +

Eigene Exceptions werfen

+

Mit throw löst du selbst einen Fehler aus, wenn etwas nicht stimmt:

+
function alterPruefen(int $alter): void {
+    if ($alter < 0) {
+        throw new InvalidArgumentException("Alter negativ");
+    }
+}
+ +

finally

+

Der finally-Block läuft immer – egal ob ein Fehler auftrat. Ideal zum Aufräumen:

+
try {
+    $datei = fopen("log.txt", "a");
+    // ... schreiben ...
+} finally {
+    fclose($datei);   // immer schließen
+}
+ +

Die Exception-Hierarchie

+

Alle Fehlerklassen erben von Throwable. Wichtig zu wissen:

+
    +
  • Exception – „normale" Fehler, die man abfangen sollte
  • +
  • Error – schwere PHP-Fehler (Typfehler, fehlende Funktion)
  • +
  • Spezialisierte Klassen wie RuntimeException, LogicException
  • +
+
+
!
+
Nicht alles wegfangenEin leeres catch, das Fehler verschluckt, ist gefährlich – du merkst nie, dass etwas schiefging. Fange nur Fehler ab, auf die du sinnvoll reagieren kannst, und logge oder melde den Rest.
+
+
+
+
+ Kapitel 11 +

Datum & Zeit

+
+ +

Mit Datum und Zeit zu rechnen ist erstaunlich fehleranfällig – Zeitzonen, Schaltjahre, Sommerzeit. PHP nimmt dir das mit der DateTime-Klasse ab.

+ +

Schnelle Ausgabe mit date()

+
echo date("d.m.Y");        // 29.05.2026
+echo date("H:i");          // 14:30
+ +

DateTime – der robuste Weg

+
$jetzt = new DateTime();
+$termin = new DateTime("2026-12-24 18:00");
+
+echo $termin->format("d.m.Y");   // 24.12.2026
+ +

Rechnen mit Intervallen

+
$heute = new DateTime();
+$heute->modify("+2 weeks");
+
+$diff = $heute->diff(new DateTime("2026-12-31"));
+echo $diff->days . " Tage";
+ +

Zeitzonen

+
$tz = new DateTimeZone("Europe/Berlin");
+$d = new DateTime("now", $tz);
+
+
+
Immer DateTimeImmutableEs gibt DateTime und DateTimeImmutable. Bei DateTime verändert modify() das Objekt selbst – das führt zu Überraschungen. Nutze besser DateTimeImmutable: Operationen geben ein neues Objekt zurück und lassen das Original unangetastet.
+
+
+
+
+ Kapitel 12 +

Dateien lesen & schreiben

+
+ +

Daten dauerhaft speichern heißt oft: in eine Datei schreiben. PHP bietet dafür angenehm kurze Funktionen.

+ +

Komplette Datei – die einfachen Funktionen

+
// ganze Datei lesen
+$inhalt = file_get_contents("notizen.txt");
+
+// ganze Datei schreiben (überschreibt)
+file_put_contents("notizen.txt", "Hallo\n");
+
+// anhängen statt überschreiben
+file_put_contents("log.txt", "Eintrag\n", FILE_APPEND);
+ +

Zeile für Zeile

+

Bei großen Dateien liest du nicht alles auf einmal in den Speicher, sondern Zeile für Zeile:

+
$f = fopen("gross.csv", "r");
+while (($zeile = fgets($f)) !== false) {
+    echo trim($zeile);
+}
+fclose($f);
+ +

Prüfen, ob etwas existiert

+
if (file_exists("config.php")) { /* ... */ }
+if (is_dir("uploads"))       { /* ... */ }
+ +

Verzeichnisse

+
$dateien = glob("bilder/*.jpg");
+foreach ($dateien as $pfad) {
+    echo basename($pfad) . "\n";
+}
+
+
!
+
Pfade & RechteSchreibzugriff scheitert oft an Dateirechten oder falschen Pfaden. Nutze absolute Pfade über __DIR__ (das Verzeichnis der aktuellen Datei), z. B. __DIR__ . "/data/log.txt", statt dich auf das Arbeitsverzeichnis zu verlassen.
+
+
+
+
+ Kapitel 13 +

JSON & Datenformate

+
+ +

JSON ist das Standardformat für den Datenaustausch im Web. PHP wandelt Arrays und Objekte mit zwei Funktionen hin und her.

+ +

PHP → JSON

+
$daten = [
+    "name" => "Marek",
+    "tags" => ["php", "godot"],
+];
+$json = json_encode($daten, JSON_PRETTY_PRINT);
+echo $json;
+
{
+    "name": "Marek",
+    "tags": ["php", "godot"]
+}
+ +

JSON → PHP

+

Mit true als zweitem Argument bekommst du ein assoziatives Array statt eines Objekts:

+
$arr = json_decode($json, true);
+echo $arr["name"];   // Marek
+ +

Fehler erkennen

+

Ist das JSON kaputt, liefert json_decode null. Sauberer: per Flag eine Exception werfen lassen:

+
try {
+    $arr = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
+} catch (JsonException $e) {
+    echo "Ungültiges JSON";
+}
+
+
i
+
Weitere FormateFür CSV gibt es fgetcsv() und fputcsv(). XML liest man am robustesten mit SimpleXML oder der DOM-Erweiterung. JSON ist aber im modernen Web der Normalfall.
+
+
+
+
+ Kapitel 14 +

Composer & Autoloading

+
+ +

Composer ist PHPs Paketmanager – das Tor zur riesigen Bibliothekswelt. Er installiert fremden Code und lädt deine eigenen Klassen automatisch.

+ +

Ein Projekt starten

+
composer init
+# beantwortet ein paar Fragen, erzeugt composer.json
+ +

Pakete installieren

+
composer require guzzlehttp/guzzle
+# lädt das Paket + Abhängigkeiten nach vendor/
+

Composer legt zwei Dateien an: composer.json (was du willst) und composer.lock (welche Versionen exakt installiert sind). Beide gehören ins Git-Repository, der Ordner vendor/ nicht.

+ +

Autoloading – nie wieder require

+

Das Herzstück: Du bindest eine Datei ein, und alle Klassen laden sich bei Bedarf selbst:

+
require "vendor/autoload.php";
+
+$client = new GuzzleHttp\Client();
+ +

Eigene Klassen automatisch laden (PSR-4)

+

In composer.json verknüpfst du einen Namespace mit einem Ordner:

+
{
+  "autoload": {
+    "psr-4": { "App\\": "src/" }
+  }
+}
+

Danach einmal composer dump-autoload – und die Klasse App\Service\Mailer wird automatisch in src/Service/Mailer.php gesucht.

+
+
+
Die zwei Befehle, die du brauchstIm Alltag reichen oft composer require paket zum Hinzufügen und composer install zum Einrichten eines geklonten Projekts. Mit --dev markierst du Werkzeuge (Tests, Linter), die nur in der Entwicklung gebraucht werden.
+
+
+
+
Teil 3
+

Typsystem & moderne Features

+
Das, was modernes PHP von altem unterscheidet: ein ausdrucksstarkes Typsystem, Enums, Match, der Pipe-Operator und benannte Argumente.
+
15 · Typen & strict_types16 · Union-, Nullable- & spezielle Typen17 · Enums18 · Moderne Syntax-Schmankerl
+
+
+
+ Kapitel 15 +

Typen & strict_types

+
+ +

PHP ist dynamisch typisiert, aber du kannst – und solltest – Typen explizit angeben. Das macht Code sicherer und für Editoren verständlich.

+ +

Skalare Typen

+

Die Grundtypen für Parameter, Rückgaben und Eigenschaften:

+ + + + + + + +
TypBeispielwert
int42
float3.14
string"text"
booltrue
array[1, 2]
+
function rabatt(float $preis, int $prozent): float {
+    return $preis * (1 - $prozent / 100);
+}
+ +

strict_types

+

Standardmäßig wandelt PHP Typen weich um: Ein String "5" wird für einen int-Parameter zu 5. Mit dieser Zeile ganz oben in der Datei schaltest du das ab:

+
<?php
+declare(strict_types=1);
+

Jetzt muss der Typ exakt passen, sonst gibt es einen TypeError. Das deckt Fehler früh auf.

+ +

Typen für Eigenschaften

+
class Konto {
+    public float $saldo = 0.0;
+    public string $inhaber;
+}
+
+
+
strict_types in jede DateiMach declare(strict_types=1); zur Gewohnheit – als erste Zeile jeder PHP-Datei. Die wenigen Stellen, an denen es dich zwingt, einen Wert bewusst zu casten, sind genau die Stellen, an denen sonst stille Bugs entstehen.
+
+
+
+
+ Kapitel 16 +

Union-, Nullable- & spezielle Typen

+
+ +

Das Typsystem kann mehr als einzelne Grundtypen: Es drückt aus „dieses oder jenes", „dieses oder nichts" und besondere Fälle wie „gibt nie zurück".

+ +

Nullable: Wert oder null

+

Ein ? vor dem Typ erlaubt zusätzlich null – typisch für „nicht gefunden":

+
function finde(int $id): ?User {
+    // gibt User oder null zurück
+}
+ +

Union: mehrere erlaubte Typen

+
function id(int|string $wert): string {
+    return (string) $wert;
+}
+ +

Spezielle Rückgabetypen

+ + + + + + +
TypBedeutung
voidgibt nichts zurück
neverkehrt nie zurück (wirft / beendet)
mixedjeder beliebige Typ
self / staticdie eigene Klasse
+
function abbruch(string $msg): never {
+    throw new RuntimeException($msg);
+}
+ +

Intersection-Typen

+

Seit PHP 8.1: ein Wert, der mehrere Interfaces zugleich erfüllt:

+
function verarbeite(Countable&Iterator $x): void { /* ... */ }
+
+
i
+
mixed sparsam einsetzenmixed bedeutet „alles erlaubt” und schaltet damit den Schutz des Typsystems faktisch ab. Es ist gelegentlich nötig (z. B. bei generischen Containern), sollte aber die Ausnahme bleiben – je präziser dein Typ, desto mehr Fehler fängt PHP für dich ab.
+
+
+
+
+ Kapitel 17 +

Enums

+
+ +

Ein Enum ist ein eigener Typ mit einer festen, abgeschlossenen Menge möglicher Werte. Statt loser Strings wie "aktiv" bekommst du echte, typsichere Optionen.

+ +

Das Problem ohne Enums

+

Früher übergab man Status als String – fehleranfällig, weil Tippfehler erst zur Laufzeit auffallen:

+
$status = "aktif";   // Tippfehler – PHP merkt nichts
+ +

Reines Enum

+
enum Status {
+    case Aktiv;
+    case Pausiert;
+    case Gesperrt;
+}
+
+function setze(Status $s): void { /* ... */ }
+setze(Status::Aktiv);   // nur gültige Werte möglich
+ +

Backed Enum – mit Wert hinterlegt

+

Wenn der Wert in einer Datenbank oder API auftaucht, hinterlegst du ihn:

+
enum Rolle: string {
+    case Admin  = "admin";
+    case Editor = "editor";
+    case Gast   = "guest";
+}
+
+$r = Rolle::from("admin");   // aus DB-Wert
+echo $r->value;             // "admin"
+echo $r->name;              // "Admin"
+ +

Methoden im Enum

+

Enums dürfen Methoden haben – ideal für ableitbare Eigenschaften:

+
enum Ampel: string {
+    case Rot  = "rot";
+    case Gruen = "gruen";
+
+    public function darfFahren(): bool {
+        return $this === Ampel::Gruen;
+    }
+}
+
+
+
Enum + match = unschlagbarEnums spielen perfekt mit match zusammen: Da die Werte abgeschlossen sind, kann dein Editor warnen, wenn du einen Fall vergisst. Ersetze lose String-Konstanten in deinem Code nach und nach durch Enums.
+
+
+
+
+ Kapitel 18 +

Moderne Syntax-Schmankerl

+
+ +

PHP 8 hat die Sprache spürbar moderner gemacht. Diese Features schreiben kürzeren, klareren Code – und sind heute Best Practice.

+ +

Benannte Argumente

+

Argumente per Namen übergeben – die Reihenfolge wird egal, optionale Werte überspringbar:

+
erstelle(name: "Box", hoehe: 10, farbe: "blau");
+ +

Der Nullsafe-Operator

+

?-> bricht eine Kette ab, sobald ein Glied null ist – statt einen Fehler zu werfen:

+
$land = $user?->adresse()?->land;
+// null, falls user oder adresse() null ist
+ +

Der Pipe-Operator (PHP 8.5)

+

Ganz neu: |> reicht einen Wert durch eine Kette von Funktionen – von links nach rechts lesbar statt verschachtelt:

+
// vorher: tief verschachtelt
+$r = array_sum(array_map(fn($x) => $x * 2, $zahlen));
+
+// mit Pipe: von links nach rechts
+$r = $zahlen
+    |> fn($a) => array_map(fn($x) => $x * 2, $a)
+    |> array_sum(...);
+ +

First-class Callable Syntax

+

Eine Funktion als Wert weiterreichen, ohne sie aufzurufen – das (...) macht's:

+
$fn = strtoupper(...);
+echo $fn("hallo");   // HALLO
+
+$gross = array_map(strtoupper(...), $woerter);
+ +

Destructuring

+

Array-Werte in einem Schritt auf Variablen verteilen:

+
[$jahr, $monat, $tag] = [2026, 5, 29];
+["name" => $name] = $person;
+
+
+
Pipe-Operator: brandneuDer |>-Operator kam erst mit PHP 8.5 (Ende 2025). Er ist großartig für Datentransformationen, aber prüfe vor dem Einsatz, dass deine Zielumgebung wirklich auf 8.5 läuft – auf älteren Versionen ist es schlicht ein Syntaxfehler.
+
+
+
+
Teil 4
+

Objektorientierung

+
Klassen und Objekte sind das Rückgrat größerer PHP-Programme. Von der ersten Klasse über Vererbung und Interfaces bis zu Traits und der Magie hinter den Kulissen.
+
19 · Klassen & Objekte20 · Sichtbarkeit & Kapselung21 · Konstruktoren modern22 · Vererbung23 · Abstrakte Klassen & Interfaces24 · Traits25 · Statisches & Konstanten26 · Magische Methoden
+
+
+
+ Kapitel 19 +

Klassen & Objekte

+
+ +

Eine Klasse ist ein Bauplan, ein Objekt das fertige Ding. Die Klasse beschreibt, welche Daten (Eigenschaften) und welche Fähigkeiten (Methoden) etwas hat.

+ +

Erste Klasse

+
class Hund {
+    public string $name;
+
+    public function bellen(): string {
+        return $this->name . " sagt Wuff!";
+    }
+}
+ +

Objekte erzeugen

+

Mit new erstellst du eine konkrete Instanz. Auf Eigenschaften und Methoden greifst du mit -> zu:

+
$bello = new Hund();
+$bello->name = "Bello";
+echo $bello->bellen();   // Bello sagt Wuff!
+ +

$this – das Objekt selbst

+

Innerhalb einer Methode verweist $this auf das aktuelle Objekt. So greift eine Methode auf die Eigenschaften ihres eigenen Objekts zu.

+ +

Mehrere unabhängige Objekte

+
$a = new Hund(); $a->name = "Rex";
+$b = new Hund(); $b->name = "Luna";
+// $a und $b sind komplett getrennt
+
+
i
+
Klasse vs. ObjektDie Klasse Hund ist der Bauplan – sie existiert einmal. Jedes mit new erzeugte Objekt ist eine eigenständige Instanz mit eigenen Daten. Aus einem Bauplan baust du beliebig viele Häuser.
+
+
+
+
+ Kapitel 20 +

Sichtbarkeit & Kapselung

+
+ +

Nicht jeder Teil eines Objekts soll von außen erreichbar sein. Sichtbarkeits-Modifikatoren schützen die innere Logik – das nennt man Kapselung.

+ +

Die drei Stufen

+ + + + + +
ModifikatorZugriff von …
publicüberall
protectedKlasse + Unterklassen
privatenur dieser Klasse selbst
+ +

Warum kapseln?

+

Eine private Eigenschaft kann nicht von außen in einen ungültigen Zustand gebracht werden. Du steuerst den Zugriff über Methoden:

+
class Konto {
+    private float $saldo = 0;
+
+    public function einzahlen(float $betrag): void {
+        if ($betrag <= 0) {
+            throw new InvalidArgumentException("> 0 nötig");
+        }
+        $this->saldo += $betrag;
+    }
+
+    public function saldo(): float {
+        return $this->saldo;
+    }
+}
+

Von außen kann niemand $konto->saldo = -999 setzen – der Weg führt nur über einzahlen(), das prüft.

+ +

Asymmetrische Sichtbarkeit (PHP 8.4)

+

Neu: von außen lesbar, aber nur intern schreibbar – in einer Zeile:

+
class User {
+    public private(set) string $id;
+}
+// $user->id lesen: ok / setzen von außen: Fehler
+
+
+
Standard: so privat wie möglichEine gute Faustregel: Mach alles private, bis du einen Grund hast, es zu öffnen. Je kleiner die öffentliche Oberfläche einer Klasse, desto leichter kannst du ihre Interna später ändern, ohne anderen Code zu brechen.
+
+
+
+
+ Kapitel 21 +

Konstruktoren modern

+
+ +

Der Konstruktor läuft automatisch beim Erzeugen eines Objekts. PHP 8 hat ihn drastisch verkürzt – das spart viel Tipparbeit.

+ +

Klassischer Konstruktor

+
class Punkt {
+    public float $x;
+    public float $y;
+
+    public function __construct(float $x, float $y) {
+        $this->x = $x;
+        $this->y = $y;
+    }
+}
+ +

Constructor Property Promotion

+

Dasselbe in modern: Sichtbarkeit direkt in die Parameterliste schreiben – Eigenschaft und Zuweisung entstehen automatisch:

+
class Punkt {
+    public function __construct(
+        public float $x,
+        public float $y,
+    ) {}
+}
+$p = new Punkt(3, 4);
+echo $p->x;   // 3
+

Beide Versionen sind exakt gleichwertig – die zweite ist nur viel kürzer.

+ +

readonly für Unveränderliches

+

Eine readonly-Eigenschaft darf nach dem Setzen im Konstruktor nicht mehr geändert werden – perfekt für Wertobjekte:

+
class Geld {
+    public function __construct(
+        public readonly int $cent,
+        public readonly string $waehrung,
+    ) {}
+}
+$preis = new Geld(999, "EUR");
+// $preis->cent = 0;  -> Error
+
+
+
readonly classes (PHP 8.2)Statt jede Eigenschaft einzeln als readonly zu markieren, kannst du seit 8.2 die ganze Klasse so deklarieren: readonly class Geld { ... }. Ideal für DTOs und Wertobjekte, die nach Erzeugung unveränderlich bleiben sollen.
+
+
+
+
+ Kapitel 22 +

Vererbung

+
+ +

Vererbung lässt eine Klasse die Eigenschaften und Methoden einer anderen übernehmen – und erweitern. So vermeidest du Wiederholung bei verwandten Typen.

+ +

extends

+
class Tier {
+    public function __construct(public string $name) {}
+    public function geraeusch(): string { return "..."; }
+}
+
+class Katze extends Tier {
+    public function geraeusch(): string { return "Miau"; }
+}
+
+$mieze = new Katze("Minka");
+echo $mieze->name;          // von Tier geerbt
+echo $mieze->geraeusch();    // Miau (überschrieben)
+ +

parent:: – die Elternversion aufrufen

+

Beim Überschreiben kannst du die ursprüngliche Methode trotzdem mitnutzen:

+
class Hund extends Tier {
+    public function __construct(string $name, public string $rasse) {
+        parent::__construct($name);
+    }
+}
+ +

final – Vererbung stoppen

+

final verhindert, dass eine Klasse erweitert oder eine Methode überschrieben wird:

+
final class Uuid { /* niemand darf erben */ }
+
+
!
+
Vererbung mit BedachtTiefe Vererbungsbäume werden schnell unübersichtlich. Eine erprobte Regel lautet „Komposition vor Vererbung”: Statt von einer Klasse zu erben, gib deinem Objekt das andere Objekt als Eigenschaft mit. Vererbung passt nur, wenn wirklich eine „ist ein”-Beziehung besteht (eine Katze ist ein Tier).
+
+
+
+
+ Kapitel 23 +

Abstrakte Klassen & Interfaces

+
+ +

Beide definieren Verträge: „Wer das sein will, muss diese Methoden bieten." Interfaces beschreiben reine Fähigkeiten, abstrakte Klassen liefern zusätzlich teilweise Umsetzung.

+ +

Interface – der reine Vertrag

+

Ein Interface listet Methoden ohne Rumpf. Jede Klasse, die es implements, muss sie ausfüllen:

+
interface Zahlbar {
+    public function betrag(): int;
+}
+
+class Rechnung implements Zahlbar {
+    public function betrag(): int { return 4200; }
+}
+

Der Gewinn: Du programmierst gegen den Vertrag, nicht gegen eine konkrete Klasse. Eine Funktion kann jedes Zahlbar entgegennehmen:

+
function verbuche(Zahlbar $x): void { /* ... */ }
+ +

Mehrere Interfaces

+
class Bestellung implements Zahlbar, JsonSerializable { /* ... */ }
+ +

Abstrakte Klasse – Vertrag plus Basis

+

Sie kann fertige Methoden mitbringen und abstrakte erzwingen. Selbst instanziieren lässt sie sich nicht:

+
abstract class Form {
+    abstract public function flaeche(): float;
+
+    public function beschreibung(): string {
+        return "Fläche: " . $this->flaeche();
+    }
+}
+
+class Kreis extends Form {
+    public function __construct(private float $r) {}
+    public function flaeche(): float { return 3.14159 * $this->r ** 2; }
+}
+
+
i
+
Wann was?Faustregel: Interface, wenn du nur eine Fähigkeit beschreibst und Klassen ganz unterschiedlicher Herkunft sie erfüllen sollen. Abstrakte Klasse, wenn verwandte Klassen gemeinsamen Code teilen und einen Pflichtteil haben. Eine Klasse kann viele Interfaces, aber nur eine (abstrakte) Elternklasse haben.
+
+
+
+
+ Kapitel 24 +

Traits

+
+ +

Ein Trait ist ein Stück wiederverwendbarer Code, das du in mehrere Klassen „hineinkopierst" – ohne Vererbung. Er löst das Problem, dass eine Klasse nur von einer Klasse erben kann.

+ +

Trait definieren und nutzen

+
trait Zeitstempel {
+    public ?DateTimeImmutable $erstellt = null;
+
+    public function jetztSetzen(): void {
+        $this->erstellt = new DateTimeImmutable();
+    }
+}
+
+class Artikel {
+    use Zeitstempel;
+}
+class Kommentar {
+    use Zeitstempel;   // gleiche Funktionalität, keine Vererbung
+}
+ +

Mehrere Traits kombinieren

+
class Post {
+    use Zeitstempel, Sluggable;
+}
+ +

Namenskonflikte auflösen

+

Bringen zwei Traits eine gleichnamige Methode mit, wählst du explizit aus:

+
class Seite {
+    use A, B {
+        A::hallo insteadof B;
+        B::hallo as halloB;
+    }
+}
+
+
!
+
Traits sparsam einsetzenTraits sind mächtig, verwischen aber die Herkunft von Code – eine Klasse kann plötzlich Methoden aus drei Dateien haben. Nutze sie für klar abgegrenzte, querschnittliche Fähigkeiten (Zeitstempel, Logging). Für echte Typ-Beziehungen sind Interfaces die sauberere Wahl.
+
+
+
+
+ Kapitel 25 +

Statisches & Konstanten

+
+ +

Manche Daten und Methoden gehören zur Klasse selbst, nicht zu einzelnen Objekten. Dafür gibt es statische Eigenschaften, Methoden und Klassen-Konstanten.

+ +

Statische Eigenschaften & Methoden

+

Zugriff erfolgt über den Klassennamen mit ::, ohne ein Objekt zu erzeugen:

+
class Zaehler {
+    public static int $anzahl = 0;
+
+    public static function hoch(): void {
+        self::$anzahl++;
+    }
+}
+Zaehler::hoch();
+echo Zaehler::$anzahl;   // 1
+ +

Klassen-Konstanten

+
class Http {
+    const OK = 200;
+    const NOT_FOUND = 404;
+}
+echo Http::NOT_FOUND;   // 404
+ +

Typisierte Konstanten (PHP 8.3)

+
class Config {
+    const string ENV = "prod";
+}
+ +

Das Named Constructor Pattern

+

Statische Methoden als sprechende Alternativen zu new:

+
class Temperatur {
+    private function __construct(public readonly float $celsius) {}
+
+    public static function ausFahrenheit(float $f): static {
+        return new self(($f - 32) * 5 / 9);
+    }
+}
+$t = Temperatur::ausFahrenheit(98.6);
+
+
i
+
static sparsamStatischer Zustand (veränderliche statische Eigenschaften) ist im Grunde globaler Zustand und erschwert Tests. Statische Methoden als Named Constructors oder reine Hilfsfunktionen sind dagegen völlig in Ordnung.
+
+
+
+
+ Kapitel 26 +

Magische Methoden

+
+ +

„Magische" Methoden beginnen mit zwei Unterstrichen und werden von PHP automatisch in bestimmten Situationen aufgerufen – etwa wenn ein Objekt als String genutzt wird.

+ +

__toString

+

Definiert, wie ein Objekt zu Text wird:

+
class Geld {
+    public function __construct(public int $cent) {}
+    public function __toString(): string {
+        return number_format($this->cent / 100, 2) . " €";
+    }
+}
+echo new Geld(1999);   // 19,99 €
+ +

__get und __set

+

Fangen Zugriffe auf nicht existierende Eigenschaften ab – Basis vieler Frameworks:

+
class Bag {
+    private array $data = [];
+    public function __get(string $k) { return $this->data[$k] ?? null; }
+    public function __set(string $k, $v) { $this->data[$k] = $v; }
+}
+$b = new Bag();
+$b->titel = "Test";   // __set
+echo $b->titel;        // __get -> Test
+ +

__call und __invoke

+

__call fängt Aufrufe nicht existierender Methoden ab; __invoke macht ein Objekt aufrufbar wie eine Funktion:

+
class Verdoppler {
+    public function __invoke(int $x): int { return $x * 2; }
+}
+$f = new Verdoppler();
+echo $f(21);   // 42
+
+
!
+
Magie versteckt LogikMagische Methoden sind elegant, aber sie machen Code schwerer nachvollziehbar – Editoren erkennen die so erzeugten Eigenschaften und Methoden oft nicht. Setze sie gezielt ein (etwa __toString für Wertobjekte) und bevorzuge sonst explizite, deklarierte Methoden.
+
+
+
+
Teil 5
+

Fortgeschrittene Sprache

+
Tieferes Sprach-Handwerk: Collections, Generatoren, Closures mit Bindung, Attribute und Reflection – die Werkzeuge hinter Frameworks.
+
27 · Collections & Generics-Denken28 · Iteratoren & Generatoren29 · Closures & Bindung30 · Attribute31 · Reflection32 · Namespaces im Detail
+
+
+
+ Kapitel 27 +

Collections & Generics-Denken

+
+ +

PHP hat keine echten Generics in der Sprache – aber das Denken in typisierten Sammlungen lohnt sich. Mit ein wenig Disziplin und Werkzeugen bekommst du fast denselben Komfort.

+ +

Das Problem mit nackten Arrays

+

Ein array sagt nichts darüber, was drinsteckt. Eine Funktion, die User[] erwartet, kann das im Typsystem nicht ausdrücken:

+
function namen(array $users): array {
+    // array von WAS? Editor weiß es nicht
+}
+ +

Lösung 1: PHPDoc-Generics

+

Statische Analyse-Werkzeuge (Teil 7) verstehen Generics in Kommentaren – die Sprache ignoriert sie, der Editor nicht:

+
/** @param User[] $users @return string[] */
+function namen(array $users): array { /* ... */ }
+ +

Lösung 2: Eine eigene Collection-Klasse

+

Du kapselst das Array in einer Klasse, die nur den gewünschten Typ akzeptiert:

+
final class UserListe implements IteratorAggregate {
+    private array $items = [];
+
+    public function add(User $u): void {
+        $this->items[] = $u;   // nur User möglich
+    }
+    public function getIterator(): Iterator {
+        return new ArrayIterator($this->items);
+    }
+}
+

Jetzt ist foreach über die Liste möglich, und niemand kann versehentlich einen String hineinlegen.

+
+
i
+
Warum keine echten Generics?Echte Generics würden tief in die PHP-Laufzeit eingreifen und kosteten Performance. Die Community löst das pragmatisch über PHPDoc plus statische Analyse – in der Praxis bekommst du damit fast die volle Typsicherheit, ohne Laufzeit-Kosten.
+
+
+
+
+ Kapitel 28 +

Iteratoren & Generatoren

+
+ +

Manchmal willst du über etwas iterieren, ohne alle Werte gleichzeitig im Speicher zu halten – etwa Millionen Zeilen aus einer Datei. Generatoren machen das mit minimalem Code.

+ +

Der yield-Befehl

+

Eine Funktion mit yield ist ein Generator: Sie liefert Werte einen nach dem anderen, pausiert dazwischen und merkt sich ihren Zustand:

+
function zaehleBis(int $max): Generator {
+    for ($i = 1; $i <= $max; $i++) {
+        yield $i;
+    }
+}
+foreach (zaehleBis(3) as $n) {
+    echo $n;   // 123
+}
+ +

Der Speicher-Vorteil

+

Eine riesige Datei zeilenweise verarbeiten, ohne sie komplett zu laden:

+
function zeilen(string $pfad): Generator {
+    $f = fopen($pfad, "r");
+    while (($z = fgets($f)) !== false) {
+        yield trim($z);
+    }
+    fclose($f);
+}
+// verbraucht konstant wenig Speicher – egal wie groß die Datei
+ +

Schlüssel mitliefern

+
yield $key => $value;
+ +

Das Iterator-Interface von Hand

+

Generatoren decken 95 % der Fälle ab. Für komplexe Iteration implementierst du Iterator direkt – mit current(), next(), valid(), key(), rewind().

+
+
+
Generatoren für große DatenmengenImmer wenn du ein großes Array baust, nur um einmal darüber zu iterieren, ist ein Generator die bessere Wahl: gleicher foreach-Komfort, aber konstanter Speicherverbrauch statt linear wachsendem.
+
+
+
+
+ Kapitel 29 +

Closures & Bindung

+
+ +

Eine Closure ist eine anonyme Funktion, die sich Werte aus ihrer Umgebung „merkt". Sie ist die Grundlage von Callbacks, Event-Handlern und vielem Framework-Code.

+ +

use – Werte einfangen

+

Mit use nimmt eine anonyme Funktion Variablen von außen mit hinein:

+
$faktor = 3;
+$mal = function($x) use ($faktor) {
+    return $x * $faktor;
+};
+echo $mal(5);   // 15
+ +

Arrow Functions fangen automatisch

+

fn braucht kein use – es sieht die umgebenden Variablen automatisch (nur lesend):

+
$mal = fn($x) => $x * $faktor;
+ +

by-reference einfangen

+

Mit & teilt die Closure dieselbe Variable, statt eine Kopie zu nehmen:

+
$summe = 0;
+$add = function($n) use (&$summe) {
+    $summe += $n;
+};
+$add(10); $add(5);
+echo $summe;   // 15
+ +

$this binden

+

Closures kennen das Objekt, in dem sie erzeugt wurden – mit bindTo() kannst du sie an ein anderes binden. Das nutzen Frameworks für Routing-Definitionen und Templating.

+
$closure = function() { return $this->name; };
+$gebunden = Closure::bind($closure, $objekt, Klasse::class);
+
+
i
+
Closure vs. Arrow Functionfn ist kompakt und fängt automatisch ein, kann aber nur einen Ausdruck enthalten. Die längere function() use(...)-Form erlaubt mehrere Zeilen und das Einfangen per Referenz. Für einfache Callbacks fn, für alles andere die lange Form.
+
+
+
+
+ Kapitel 30 +

Attribute

+
+ +

Attribute sind strukturierte Metadaten, die du direkt an Klassen, Methoden oder Eigenschaften heftest. Frameworks lesen sie aus, um Verhalten zu steuern – ganz ohne Konfigurationsdateien.

+ +

Syntax

+

Attribute stehen in #[ ] direkt über dem Element:

+
#[Route("/users", methods: ["GET"])]
+public function liste(): Response { /* ... */ }
+ +

Ein eigenes Attribut

+

Ein Attribut ist nichts weiter als eine Klasse mit dem Marker #[Attribute]:

+
#[Attribute(Attribute::TARGET_METHOD)]
+class Route {
+    public function __construct(
+        public string $pfad,
+        public array $methods = ["GET"],
+    ) {}
+}
+ +

Attribute auslesen

+

Über Reflection (nächstes Kapitel) liest ein Framework die Attribute zur Laufzeit aus:

+
$r = new ReflectionMethod($controller, "liste");
+foreach ($r->getAttributes(Route::class) as $attr) {
+    $route = $attr->newInstance();
+    echo $route->pfad;   // /users
+}
+
+
i
+
Wo du Attribute triffstSymfony nutzt Attribute für Routing und Validierung, Doctrine für das Mapping von Klassen auf Datenbanktabellen, PHPUnit für Test-Markierungen. Du wirst sie also oft nutzen, bevor du eigene schreibst – und genau dafür ist es gut, das Prinzip zu verstehen.
+
+
+
+
+ Kapitel 31 +

Reflection

+
+ +

Reflection erlaubt einem Programm, sich selbst zu untersuchen: Welche Methoden hat diese Klasse? Welche Parameter diese Funktion? Es ist die Magie hinter Dependency-Injection-Containern und Frameworks.

+ +

Eine Klasse inspizieren

+
$r = new ReflectionClass(User::class);
+
+echo $r->getName();              // User
+foreach ($r->getMethods() as $m) {
+    echo $m->getName() . "\n";
+}
+ +

Parameter einer Methode lesen

+

So findet ein DI-Container heraus, welche Abhängigkeiten ein Konstruktor braucht:

+
$ctor = (new ReflectionClass(Service::class))->getConstructor();
+foreach ($ctor->getParameters() as $p) {
+    echo $p->getType();   // der Typ des Parameters
+}
+ +

Objekte dynamisch erzeugen

+
$obj = (new ReflectionClass($klassenName))
+    ->newInstanceArgs($argumente);
+
+
!
+
Reflection ist langsam & mächtigReflection umgeht private und ist deutlich langsamer als direkter Code. In Anwendungscode brauchst du es fast nie – es ist Framework-Handwerk. Gut zu verstehen, um zu wissen, wie deine Werkzeuge funktionieren; im Alltag aber selten selbst zu schreiben.
+
+
+
+
+ Kapitel 32 +

Namespaces im Detail

+
+ +

Namespaces verhindern Namenskollisionen, wenn Code aus vielen Quellen zusammenkommt. Sie sind die Ordnerstruktur des Codes – und die Basis für Autoloading.

+ +

Deklaration

+

Ein Namespace steht als erste Anweisung der Datei:

+
<?php
+namespace App\Service;
+
+class Mailer { /* voll: App\Service\Mailer */ }
+ +

use – Klassen importieren

+

Statt überall den vollen Pfad zu schreiben, importierst du oben einmal:

+
use App\Service\Mailer;
+use App\Model\User;
+
+$m = new Mailer();   // kurz statt voll qualifiziert
+ +

Aliasse bei Kollisionen

+

Heißen zwei Klassen gleich, gibst du einer per as einen anderen Namen:

+
use App\Pdf\Writer as PdfWriter;
+use App\Csv\Writer as CsvWriter;
+ +

Funktionen und Konstanten importieren

+
use function App\Helpers\slugify;
+use const App\Config\VERSION;
+ +

Der führende Backslash

+

Ein \ am Anfang meint „ab dem globalen Namespace". Innerhalb eines eigenen Namespaces brauchst du es, um auf eingebaute Klassen zuzugreifen:

+
namespace App;
+$d = new \DateTime();   // global, nicht App\DateTime
+
+
+
PSR-4: Namespace = OrdnerHalte dich an die PSR-4-Konvention: Der Namespace spiegelt den Ordnerpfad. App\Service\Mailer liegt in src/Service/Mailer.php. Dann findet Composers Autoloader jede Klasse automatisch – du schreibst nie wieder ein require für Klassen.
+
+
+
+
Teil 6
+

Architektur & Patterns

+
Wie man Code so strukturiert, dass er wachsen, sich ändern und testen lässt. SOLID, Dependency Injection, die wichtigsten Entwurfsmuster und Wertobjekte.
+
33 · SOLID-Prinzipien34 · Dependency Injection35 · Häufige Entwurfsmuster36 · Wertobjekte & DTOs37 · Fehlerbehandlung als Architektur
+
+
+
+ Kapitel 33 +

SOLID-Prinzipien

+
+ +

SOLID sind fünf Faustregeln für Klassen, die langlebig und änderbar bleiben. Sie sind keine Gesetze, sondern Werkzeuge gegen den schleichenden Verfall großer Codebasen.

+ +

S – Single Responsibility

+

Eine Klasse sollte genau einen Grund haben, sich zu ändern. Eine Klasse, die Daten lädt und formatiert und verschickt, ändert sich aus drei Richtungen – das ist eine zu viel.

+ +

O – Open/Closed

+

Offen für Erweiterung, geschlossen für Änderung. Neues Verhalten fügst du durch neue Klassen hinzu, nicht durch das Aufbohren bestehender:

+
interface Rabatt {
+    public function anwenden(float $preis): float;
+}
+// neuer Rabatt = neue Klasse, alter Code bleibt unberührt
+class Weihnachtsrabatt implements Rabatt { /* ... */ }
+ +

L – Liskov Substitution

+

Eine Unterklasse muss überall einsetzbar sein, wo die Oberklasse erwartet wird – ohne Überraschungen. Wenn eine Unterklasse plötzlich Ausnahmen wirft, wo die Basis es nicht tut, ist das verletzt.

+ +

I – Interface Segregation

+

Lieber viele kleine, spezifische Interfaces als ein großes. Eine Klasse soll nicht Methoden implementieren müssen, die sie gar nicht braucht.

+ +

D – Dependency Inversion

+

Hänge von Abstraktionen ab, nicht von konkreten Klassen. Statt im Code new MySQLConnection() zu schreiben, nimm ein Connection-Interface entgegen – das ist die Brücke zu Dependency Injection.

+
+
i
+
SOLID mit AugenmaßSOLID hilft, aber dogmatisch angewandt führt es zu einer Flut winziger Klassen. Die Prinzipien zahlen sich dort aus, wo sich Code oft ändert oder von mehreren Stellen genutzt wird. Bei kurzlebigem oder trivialem Code ist Pragmatismus die bessere Tugend.
+
+
+
+
+ Kapitel 34 +

Dependency Injection

+
+ +

Dependency Injection (DI) heißt schlicht: Ein Objekt bekommt seine Abhängigkeiten von außen gereicht, statt sie selbst zu erzeugen. Das macht Code testbar und flexibel.

+ +

Das Problem

+

Erzeugt eine Klasse ihre Helfer selbst, ist sie fest verdrahtet – nicht austauschbar, nicht testbar:

+
class Bestellung {
+    public function __construct() {
+        $this->mailer = new SmtpMailer();   // fest verdrahtet
+    }
+}
+ +

Die Lösung: von außen reichen

+
class Bestellung {
+    public function __construct(
+        private Mailer $mailer,   // Interface, injiziert
+    ) {}
+}
+// Produktion:
+new Bestellung(new SmtpMailer());
+// Test:
+new Bestellung(new FakeMailer());
+ +

Der DI-Container

+

Bei vielen verschachtelten Abhängigkeiten verdrahtest du nicht alles von Hand. Ein Container baut Objekte samt ihrer Abhängigkeiten automatisch zusammen:

+
$container->get(Bestellung::class);
+// Container erkennt: braucht Mailer, baut SmtpMailer, ...
+

Container lesen die nötigen Typen über Reflection (Kapitel 31) aus den Konstruktor-Signaturen. Genau deshalb sind Typdeklarationen so wichtig.

+
+
+
Constructor Injection als StandardReiche Abhängigkeiten über den Konstruktor herein – nicht über Setter oder globale Zugriffe. Dann ist an der Signatur sofort ablesbar, was eine Klasse braucht, und ein unvollständig konfiguriertes Objekt kann gar nicht erst entstehen.
+
+
+
+
+ Kapitel 35 +

Häufige Entwurfsmuster

+
+ +

Entwurfsmuster sind bewährte Lösungen für wiederkehrende Probleme. Du musst sie nicht auswendig kennen – aber die Namen helfen, über Architektur zu reden.

+ +

Strategy

+

Austauschbare Algorithmen hinter einem gemeinsamen Interface – das Open/Closed-Prinzip in Aktion:

+
interface SortierStrategie {
+    public function sortiere(array $daten): array;
+}
+// QuickSort, MergeSort … jeweils eine Klasse, frei tauschbar
+ +

Factory

+

Eine Methode, die Objekte erzeugt und die Entscheidung kapselt, welche konkrete Klasse:

+
class ZahlungsFactory {
+    public static function erstelle(string $art): Zahlung {
+        return match($art) {
+            "paypal" => new PayPal(),
+            "karte"  => new Kreditkarte(),
+        };
+    }
+}
+ +

Observer

+

Objekte abonnieren Ereignisse und werden benachrichtigt – die Basis von Event-Systemen:

+
interface Beobachter {
+    public function benachrichtigt(Ereignis $e): void;
+}
+ +

Repository

+

Kapselt den Datenzugriff hinter einer sammlungsartigen Schnittstelle – der Code dahinter (SQL, API, Datei) bleibt verborgen:

+
interface UserRepository {
+    public function finde(int $id): ?User;
+    public function speichere(User $u): void;
+}
+
+
!
+
Muster sind Mittel, kein ZielDer häufigste Fehler mit Entwurfsmustern ist, sie überall einzusetzen, weil man sie gerade gelernt hat. Ein Muster ist eine Antwort auf ein konkretes Problem. Hast du das Problem nicht, brauchst du das Muster nicht – einfacher Code schlägt clevere Architektur ohne Anlass.
+
+
+
+
+ Kapitel 36 +

Wertobjekte & DTOs

+
+ +

Statt nackte Strings und Zahlen durch den Code zu reichen, kapselst du Bedeutung in kleine Typen. Das macht Fehler unmöglich, die sonst erst zur Laufzeit auffallen.

+ +

Das Problem mit Primitiven

+
function versende(string $email): void { /* ... */ }
+versende("kein-email");   // kompiliert, kracht erst später
+ +

Ein Wertobjekt

+

Validiere einmal bei der Erzeugung – danach ist die Gültigkeit garantiert. readonly macht es unveränderlich:

+
final readonly class Email {
+    public function __construct(public string $wert) {
+        if (!filter_var($wert, FILTER_VALIDATE_EMAIL)) {
+            throw new InvalidArgumentException("Ungültige E-Mail");
+        }
+    }
+}
+function versende(Email $email): void { /* immer gültig */ }
+ +

DTO – Data Transfer Object

+

Ein DTO bündelt zusammengehörige Daten in einem typisierten Objekt – etwa Formulareingaben oder API-Antworten. Kein Verhalten, nur Struktur:

+
final readonly class RegistrierDaten {
+    public function __construct(
+        public string $name,
+        public Email $email,
+        public int $alter,
+    ) {}
+}
+
+
+
Primitive Obsession bekämpfenDas Durchreichen von rohen Strings und Ints für Dinge mit Bedeutung (E-Mail, Geldbetrag, ID) nennt man „Primitive Obsession”. Ein kleines Wertobjekt kostet wenige Zeilen, verlagert aber eine ganze Klasse von Fehlern von der Laufzeit in den Moment der Erzeugung.
+
+
+
+
+ Kapitel 37 +

Fehlerbehandlung als Architektur

+
+ +

Wie eine Anwendung mit Fehlern umgeht, ist eine Architektur-Entscheidung. Gut gemacht, bleiben Fehler nachvollziehbar und die Geschäftslogik sauber.

+ +

Eigene Exception-Typen

+

Spezifische Exceptions erlauben gezieltes Abfangen und sprechende Fehler:

+
class KontoUeberzogen extends DomainException {
+    public static function bei(float $fehlbetrag): self {
+        return new self("Es fehlen $fehlbetrag €");
+    }
+}
+ +

Wo fangen, wo durchreichen?

+

Eine bewährte Regel: Fange Exceptions möglichst weit oben – an der „Grenze" der Anwendung (Controller, CLI-Einstieg). Die Geschäftslogik wirft nur, sie behandelt nicht. So bleibt sie frei von try/catch-Rauschen:

+
// Controller – die eine zentrale Stelle
+try {
+    $service->verarbeite($daten);
+} catch (DomainException $e) {
+    return fehlerSeite($e->getMessage());
+}
+ +

Result statt Exception

+

Für erwartbare Fehlschläge (Validierung) sind Exceptions manchmal zu schwer. Eine Alternative ist ein Ergebnis-Objekt, das Erfolg oder Fehler trägt:

+
if ($ergebnis->istErfolg()) {
+    $wert = $ergebnis->wert();
+} else {
+    echo $ergebnis->fehler();
+}
+
+
i
+
Exceptions für AusnahmenDer Name sagt es: Exceptions sind für Ausnahmen gedacht – Dinge, die nicht im normalen Ablauf liegen. Ein fehlgeschlagenes Login ist erwartbar und gehört eher als Ergebnis modelliert; eine verlorene Datenbankverbindung ist eine echte Ausnahme. Diese Unterscheidung hält den Kontrollfluss klar.
+
+
+
+
Teil 7
+

Qualität & Profi-Werkzeug

+
Was professionellen Code von Bastelei trennt: automatisierte Tests, statische Analyse, einheitlicher Stil und systematisches Debugging.
+
38 · Testen mit PHPUnit39 · Statische Analyse40 · Code-Style & Tooling41 · Debugging & Xdebug
+
+
+
+ Kapitel 38 +

Testen mit PHPUnit

+
+ +

Automatisierte Tests prüfen bei jeder Änderung, ob dein Code noch das Richtige tut. PHPUnit ist der De-facto-Standard dafür in der PHP-Welt.

+ +

Installation

+
composer require --dev phpunit/phpunit
+ +

Ein erster Test

+

Ein Test ist eine Methode, die etwas ausführt und mit assert-Aufrufen das erwartete Ergebnis prüft:

+
use PHPUnit\Framework\TestCase;
+
+final class RechnerTest extends TestCase {
+    public function testAddition(): void {
+        $r = new Rechner();
+        $this->assertSame(5, $r->addiere(2, 3));
+    }
+}
+

Ausführen:

+
vendor/bin/phpunit
+# OK (1 test, 1 assertion)
+ +

Die wichtigsten Assertions

+ + + + + + + + +
AssertionPrüft
assertSame($a, $b)identisch (===)
assertEquals($a, $b)gleich (==)
assertTrue / assertFalseWahrheitswert
assertNullist null
assertCount(3, $arr)Anzahl Elemente
expectException(...)Fehler wird geworfen
+ +

Data Provider – ein Test, viele Fälle

+

Statt fünf fast gleiche Tests zu schreiben, fütterst du einen Test mit Datensätzen:

+
#[DataProvider("zahlen")]
+public function testQuadrat(int $ein, int $aus): void {
+    $this->assertSame($aus, quadrat($ein));
+}
+public static function zahlen(): array {
+    return [[2, 4], [3, 9], [5, 25]];
+}
+ +

Mocks – Abhängigkeiten ersetzen

+

Um eine Klasse isoliert zu testen, ersetzt du ihre Abhängigkeiten durch kontrollierte Attrappen:

+
$mailer = $this->createMock(Mailer::class);
+$mailer->expects($this->once())->method("sende");
+
+
+
Erst der Test, dann der FehlerWenn du einen Bug findest, schreib zuerst einen Test, der ihn reproduziert – er schlägt fehl. Dann reparierst du den Code, bis der Test grün ist. So weißt du sicher, dass der Fehler weg ist und nie unbemerkt zurückkehrt.
+
+
+
+
+ Kapitel 39 +

Statische Analyse

+
+ +

Statische Analyse findet Fehler, ohne den Code auszuführen – allein durch das Lesen. Werkzeuge wie PHPStan und Psalm fangen ganze Fehlerklassen ab, bevor sie je zur Laufzeit auftreten.

+ +

Was sie finden

+
    +
  • Aufrufe nicht existierender Methoden oder Eigenschaften
  • +
  • Typfehler – ein string, wo ein int erwartet wird
  • +
  • null-Zugriffe, die zur Laufzeit krachen würden
  • +
  • toter Code, unerreichbare Zweige
  • +
+ +

PHPStan einsetzen

+
composer require --dev phpstan/phpstan
+vendor/bin/phpstan analyse src --level 6
+

Die Level reichen von 0 (locker) bis 10 (sehr streng). Ein guter Weg: niedrig starten und Stufe für Stufe erhöhen, während du die gemeldeten Probleme abarbeitest.

+ +

Generics über PHPDoc

+

Hier zahlt sich das Generics-Denken aus Kapitel 27 aus. PHPStan versteht die Annotationen und prüft sie:

+
/** @return list<User> */
+public function alle(): array { /* ... */ }
+ +

Baseline für Altprojekte

+

In bestehendem Code meldet PHPStan oft hunderte Probleme. Eine Baseline friert die aktuellen Funde ein, sodass nur neue Fehler auffallen:

+
vendor/bin/phpstan analyse --generate-baseline
+
+
+
Statische Analyse in CILass PHPStan automatisch bei jedem Push laufen (Continuous Integration). Dann kommt fehlerhafter Code gar nicht erst in den Hauptzweig. In Kombination mit declare(strict_types=1) und Tests bildet das ein engmaschiges Sicherheitsnetz.
+
+
+
+
+ Kapitel 40 +

Code-Style & Tooling

+
+ +

Einheitlicher Stil macht Code lesbar und beendet sinnlose Diskussionen über Klammern und Einrückung. Werkzeuge erledigen die Formatierung automatisch.

+ +

PSR-12 – der Standard

+

Die PHP-Community hat sich auf einen gemeinsamen Stil geeinigt: PSR-12. Er regelt Einrückung (4 Leerzeichen), Klammersetzung, Import-Reihenfolge und mehr. Du musst ihn nicht auswendig lernen – Werkzeuge setzen ihn durch.

+ +

PHP-CS-Fixer

+

Formatiert deinen Code automatisch nach festgelegten Regeln:

+
composer require --dev friendsofphp/php-cs-fixer
+vendor/bin/php-cs-fixer fix src
+ +

Composer-Skripte als Abkürzung

+

Häufige Befehle bündelst du in composer.json – ein Befehl statt vieler:

+
{
+  "scripts": {
+    "check": [
+      "@php vendor/bin/phpunit",
+      "@php vendor/bin/phpstan analyse src",
+      "@php vendor/bin/php-cs-fixer fix --dry-run"
+    ]
+  }
+}
+

Danach genügt composer check, um Tests, Analyse und Stilprüfung in einem Rutsch laufen zu lassen.

+ +

Der typische Werkzeugkasten

+ + + + + + + +
AufgabeWerkzeug
PaketeComposer
TestsPHPUnit / Pest
Statische AnalysePHPStan / Psalm
FormatierungPHP-CS-Fixer / PHPCS
DebuggingXdebug
+
+
i
+
Stil ist Konvention, nicht GeschmackWelcher Stil „besser” ist, spielt kaum eine Rolle – wichtig ist, dass ein Projekt einen hat und ihn automatisch durchsetzt. Mit einem Formatierer im Editor-Speichern-Hook denkst du nie wieder über Einrückung nach.
+
+
+
+
+ Kapitel 41 +

Debugging & Xdebug

+
+ +

Früher oder später läuft Code anders als gedacht. Statt mit echo zu raten, schaust du mit einem Debugger Schritt für Schritt zu, was wirklich passiert.

+ +

Der schnelle Weg: var_dump & dd

+

Für einen kurzen Blick reicht oft eine Ausgabe. var_dump zeigt Wert und Typ:

+
var_dump($daten);
+// in Frameworks oft: dump($daten) oder dd($daten) (dump and die)
+ +

Xdebug – der richtige Debugger

+

Xdebug ist eine PHP-Erweiterung. Einmal eingerichtet, kannst du im Editor Breakpoints setzen: Das Programm hält dort an, und du untersuchst alle Variablen live.

+
# Installation (Beispiel Ubuntu)
+sudo apt install php-xdebug
+

Im Editor (z. B. PHPStorm oder VS Code) aktivierst du „Listen for debug connections", setzt einen Breakpoint per Klick an den Zeilenrand und lädst die Seite. Der Ablauf stoppt – du siehst den kompletten Aufruf-Stack.

+ +

Die Werkzeuge im Debugger

+ + + + + + +
AktionBedeutung
Step Overnächste Zeile, Funktionen am Stück
Step Intoin die aufgerufene Funktion hinein
Step Outaktuelle Funktion zu Ende
Watcheinen Ausdruck dauerhaft beobachten
+ +

Fehler sichtbar machen

+

In der Entwicklung sollten alle Fehler angezeigt werden – verstecke sie nie still:

+
error_reporting(E_ALL);
+ini_set("display_errors", "1");
+// In Produktion: display_errors aus, dafür ins Log schreiben
+
+
!
+
echo-Debugging hat GrenzenMit eingestreuten var_dump-Zeilen kommt man weit – aber bei verschachtelten Aufrufen oder Schleifen verliert man schnell den Überblick. Die halbe Stunde, Xdebug einmal einzurichten, spart über die Zeit viele Stunden Rätselraten.
+
+
+
+
Teil 8
+

Experten & Nischen

+
Die Themen jenseits des Alltags: Performance herausholen, Speicher verstehen, Prozesse und Fibers, robuste CLI-Programme und die Sicherheits-Klassiker.
+
42 · Performance & OPcache43 · Speicher & Referenzen44 · Prozesse, FFI & Fibers45 · CLI-Programme bauen46 · Sicherheit: die Klassiker
+
+
+
+ Kapitel 42 +

Performance & OPcache

+
+ +

PHP ist schnell – wenn man es lässt. Die größten Hebel liegen selten im Mikro-Tuning einzelner Zeilen, sondern in Caching, weniger Arbeit und dem richtigen Werkzeug.

+ +

OPcache – kompilierten Code wiederverwenden

+

Normalerweise übersetzt PHP jede Datei bei jedem Aufruf neu. OPcache speichert das Übersetzungsergebnis im Speicher – ein gewaltiger Gewinn, fast geschenkt:

+
; php.ini
+opcache.enable=1
+opcache.memory_consumption=256
+opcache.max_accelerated_files=20000
+

In Produktion ist OPcache praktisch Pflicht. Es ist der wirkungsvollste einzelne Performance-Schalter.

+ +

Messen statt raten

+

Optimiere nie nach Bauchgefühl. Ein Profiler (Xdebug, Blackfire, SPX) zeigt, wo die Zeit wirklich verbraucht wird – oft an völlig anderen Stellen als vermutet:

+
$start = hrtime(true);
+// ... Code ...
+$ms = (hrtime(true) - $start) / 1_000_000;
+ +

Die häufigste Bremse: N+1

+

Ein klassischer Datenbank-Fehler: In einer Schleife für jeden Datensatz eine eigene Abfrage. 100 Nutzer = 101 Abfragen statt einer. Lade verwandte Daten gebündelt.

+ +

JIT – mit Augenmaß

+

Seit PHP 8 gibt es einen JIT-Compiler. Er beschleunigt rechenintensive Aufgaben (Bildverarbeitung, Mathematik) spürbar, bringt bei typischem Web-Code mit viel I/O aber kaum etwas.

+
+
+
Reihenfolge der OptimierungErst OPcache und einen Profiler einschalten, dann die teuersten Stellen finden, dann gezielt verbessern – meist sind es Datenbankabfragen oder fehlendes Caching. Mikro-Optimierungen einzelner Funktionen lohnen fast nie und machen den Code schwerer lesbar.
+
+
+
+
+ Kapitel 43 +

Speicher & Referenzen

+
+ +

PHP verwaltet Speicher automatisch, aber zu verstehen, wie Werte kopiert und freigegeben werden, hilft bei großen Datenmengen und kniffligen Bugs.

+ +

Copy-on-Write

+

Weist du eine Variable einer anderen zu, kopiert PHP den Wert nicht sofort – erst wenn einer der beiden geändert wird. Das spart Speicher, ohne dass du etwas tun musst:

+
$a = range(1, 1_000_000);
+$b = $a;        // noch keine Kopie, beide teilen sich
+$b[0] = 99;   // jetzt erst wird kopiert
+ +

Referenzen mit &

+

Eine Referenz ist ein zweiter Name für dieselbe Variable. Änderungen wirken auf beide:

+
$a = 1;
+$b = &$a;     // $b ist $a
+$b = 99;
+echo $a;       // 99
+ +

Garbage Collection

+

PHP zählt, wie viele Namen auf einen Wert zeigen. Sinkt der Zähler auf null, wird der Speicher frei. Zirkuläre Referenzen (A zeigt auf B, B auf A) fängt ein zusätzlicher Collector ab.

+ +

Speicher im Blick behalten

+
echo memory_get_usage(true);        // aktuell
+echo memory_get_peak_usage(true);   // Höchststand
+
+
!
+
Referenzen sind selten nötigAnfänger greifen oft zu &, um „Performance zu sparen” – dank Copy-on-Write ist das fast nie nötig und führt zu schwer auffindbaren Bugs, wenn unerwartet eine entfernte Variable mitverändert wird. Nutze Referenzen nur, wenn du sie wirklich brauchst (etwa sort(), das sein Argument verändert).
+
+
+
+
+ Kapitel 44 +

Prozesse, FFI & Fibers

+
+ +

Drei fortgeschrittene Wege, über die übliche Request-Verarbeitung hinauszugehen: externe Programme starten, C-Bibliotheken einbinden und kooperatives Multitasking.

+ +

Externe Programme starten

+

Manchmal ist das beste Werkzeug ein anderes Programm. proc_open gibt dir volle Kontrolle über dessen Ein- und Ausgabe:

+
$out = shell_exec("git rev-parse HEAD");
+// für einfache Fälle; bei Nutzereingaben unbedingt escapen!
+ +

FFI – C-Bibliotheken aufrufen

+

Mit der Foreign Function Interface bindest du vorhandene C-Bibliotheken direkt ein, ohne eine PHP-Erweiterung zu schreiben:

+
$ffi = FFI::cdef(
+    "int abs(int);",
+    "libc.so.6"
+);
+echo $ffi->abs(-42);   // 42
+

FFI ist ein Nischenwerkzeug – nützlich, um Spezialbibliotheken anzuzapfen, aber selten im Alltag.

+ +

Fibers – kooperatives Multitasking

+

Seit PHP 8.1 gibt es Fibers: Funktionen, die sich selbst pausieren und später fortsetzen können. Sie sind die Grundlage moderner asynchroner Frameworks wie ReactPHP und Amp:

+
$fiber = new Fiber(function() {
+    $wert = Fiber::suspend("pausiert");
+    echo "weiter mit $wert";
+});
+$x = $fiber->start();   // "pausiert"
+$fiber->resume("Daten");   // weiter mit Daten
+
+
i
+
Selten gebraucht, gut zu kennenFibers nutzt du fast nie direkt – die asynchronen Frameworks kapseln sie. Aber zu wissen, dass PHP kooperatives Multitasking beherrscht, hilft beim Verständnis, wie etwa Swoole oder Amp Tausende gleichzeitige Verbindungen in einem Prozess bewältigen.
+
+
+
+
+ Kapitel 45 +

CLI-Programme bauen

+
+ +

PHP ist nicht nur für Webseiten da. Kommandozeilen-Programme – für Wartung, Datenimport, Cronjobs – sind ein wichtiger Einsatzbereich.

+ +

Argumente lesen

+

Das Array $argv enthält die übergebenen Argumente, $argc ihre Anzahl:

+
// php import.php datei.csv --dry-run
+echo $argv[1];   // datei.csv
+ +

Ein-/Ausgabe-Ströme

+

CLI-Programme nutzen drei Standard-Kanäle. Fehler gehören nach STDERR, damit sie sich von der normalen Ausgabe trennen lassen:

+
fwrite(STDOUT, "Fertig\n");
+fwrite(STDERR, "Warnung!\n");
+$eingabe = fgets(STDIN);   // vom Nutzer lesen
+ +

Exit-Codes

+

Ein Programm meldet Erfolg oder Misserfolg über seinen Rückgabewert: 0 = alles gut, alles andere = Fehler. Skripte und CI verlassen sich darauf:

+
if ($fehler) {
+    fwrite(STDERR, "Abbruch\n");
+    exit(1);
+}
+exit(0);
+ +

Komfort mit Symfony Console

+

Für ernsthafte CLI-Werkzeuge lohnt eine Bibliothek. Symfony Console liefert Argument-Parsing, Hilfetexte, Farben, Fortschrittsbalken und Tabellen:

+
composer require symfony/console
+

Damit definierst du Befehle als Klassen, bekommst --help automatisch und eine saubere Struktur für wachsende Werkzeuge.

+
+
+
Shebang für direkte AusführungBeginnt dein Skript mit #!/usr/bin/env php und ist es ausführbar (chmod +x), kannst du es ohne vorangestelltes php direkt aufrufen – wie jedes andere Kommandozeilen-Programm.
+
+
+
+
+ Kapitel 46 +

Sicherheit: die Klassiker

+
+ +

Die meisten Sicherheitslücken sind seit Jahren dieselben – und seit Jahren vermeidbar. Wer diese Klassiker kennt, schließt die gefährlichsten Türen.

+ +

SQL-Injection

+

Niemals Nutzereingaben in SQL einbauen. Prepared Statements trennen Befehl und Daten – die Eingabe kann nie als Code interpretiert werden:

+
// FALSCH – angreifbar
+$db->query("SELECT * FROM users WHERE id = $id");
+
+// RICHTIG – Prepared Statement
+$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
+$stmt->execute([$id]);
+ +

XSS – Cross-Site Scripting

+

Gibst du Nutzereingaben in HTML aus, ohne sie zu maskieren, kann jemand Schadcode einschleusen. htmlspecialchars entschärft das:

+
echo htmlspecialchars($kommentar, ENT_QUOTES);
+ +

CSRF – gefälschte Anfragen

+

Ein Angreifer bringt den Browser eines eingeloggten Nutzers dazu, ungewollt Aktionen auszuführen. Schutz: ein geheimes, pro Formular einzigartiges Token, das der Server prüft.

+ +

Passwörter richtig speichern

+

Niemals im Klartext, niemals mit MD5 oder SHA1. PHP bringt die richtige Funktion mit – sie hasht, salzt und passt die Stärke automatisch an:

+
$hash = password_hash($passwort, PASSWORD_DEFAULT);
+
+if (password_verify($eingabe, $hash)) {
+    // Login korrekt
+}
+ +

Grundregeln

+ + + + + + + +
RegelWarum
Eingaben validierentraue keiner Quelle von außen
Ausgaben maskierenverhindert XSS
Prepared Statementsverhindert SQL-Injection
password_hashsichere Passwörter
Abhängigkeiten aktuell haltencomposer audit
+
+
!
+
Sicherheit ist nicht optionalValidiere alles, was von außen kommt – Formulare, URLs, Header, hochgeladene Dateien. Die Faustregel lautet: niemals Nutzereingaben vertrauen. composer audit meldet zudem bekannte Lücken in deinen Abhängigkeiten – führe es regelmäßig aus.
+
+
+ + +``` \ No newline at end of file diff --git a/templates/Referenz/MiniGuide.md b/templates/Referenz/MiniGuide.md new file mode 100644 index 0000000..a0201f3 --- /dev/null +++ b/templates/Referenz/MiniGuide.md @@ -0,0 +1,395 @@ +``` + + + + +PHP Mini-Guide + + + + + +
+ +
+

PHP in 15 Minuten

+
Dein erstes PHP-Programm – Schritt für Schritt
+
+
+ Mini-Guide + 15 Min · von Null +
+
+ + +
+ Frage zum Einstieg + PHP läuft hinter rund drei Viertel aller Webseiten – inklusive WordPress, Wikipedia und Facebook. Aber wie sieht PHP-Code überhaupt aus, und wie startet man? In 15 Minuten kannst du dein erstes Programm schreiben. +
+ + +

PHP starten

+ +

PHP ist eine Programmiersprache, die auf deinem Computer oder einem Webserver läuft. Im Gegensatz zu HTML oder CSS wird PHP nicht im Browser angezeigt – es erzeugt Ausgaben (zum Beispiel HTML), die dann ausgeliefert werden.

+ +

Um anzufangen, brauchst du PHP auf deinem Computer. Auf Mac: brew install php. Auf Ubuntu: apt install php8.4-cli. Auf Windows: am einfachsten WSL2 mit Ubuntu darin nutzen.

+ +

PHP-Code lebt in Dateien mit der Endung .php. Lege eine Datei hallo.php an mit diesem Inhalt:

+ +
<?php
+
+echo "Hallo Welt!";
+ +

Die erste Zeile <?php sagt PHP: "ab hier kommt mein Code". Das Wort echo heißt: "gib das aus, was danach kommt". Strings (also Text) stehen in Anführungszeichen. Jede Anweisung endet mit einem Semikolon.

+ +

Im Terminal ausführen mit:

+ +
php hallo.php
+ +

Du siehst "Hallo Welt!" – dein erstes PHP-Programm läuft.

+ + +

Variablen

+ +

Eine Variable ist ein benannter Platz, an dem du einen Wert speicherst. In PHP beginnen Variablen immer mit einem Dollar-Zeichen $. Das macht sie im Code sofort erkennbar:

+ +
<?php
+
+$name = "Marek";
+$alter = 34;
+$istAktiv = true;
+
+echo $name;
+ +

Variablen können verschiedene Arten von Werten enthalten. Die drei wichtigsten:

+ +
    +
  • Strings – Text in Anführungszeichen, z.B. "Hallo"
  • +
  • Zahlen – ganze Zahlen (42) oder Kommazahlen (3.14)
  • +
  • Wahrheitswertetrue (wahr) oder false (falsch)
  • +
+ +

Mit dem Punkt verbindest du Strings:

+ +
$gruss = "Hallo, " . $name . "!";
+echo $gruss;             // Hallo, Marek!
+ + +

Bedingungen

+ +

Programme müssen oft entscheiden: "wenn X zutrifft, mach Y, sonst Z". Dafür gibt es if und else:

+ +
$alter = 17;
+
+if ($alter >= 18) {
+  echo "Du bist erwachsen.";
+} else {
+  echo "Du bist noch minderjährig.";
+}
+ +

Die Klammer hinter if enthält die Bedingung. Die geschweiften Klammern { } umschließen den Code, der ausgeführt wird, wenn die Bedingung wahr ist. else ist der Block, wenn sie falsch ist.

+ +

Wichtige Vergleichs-Operatoren:

+ +
    +
  • == gleich
  • +
  • != ungleich
  • +
  • <, > kleiner, größer
  • +
  • <=, >= kleiner-gleich, größer-gleich
  • +
+ +
+
i
+
+ Ein Gleichheitszeichen reicht nicht + Zum Zuweisen nutzt du = (ein Gleichheitszeichen). Zum Vergleichen brauchst du == (zwei). Das ist eine häufige Verwechslung bei Anfängern. +
+
+ + +

Listen und Schleifen

+ +

Mehrere Werte fasst du in einer Liste zusammen. In PHP heißen Listen array:

+ +
$obst = ["Apfel", "Birne", "Kirsche"];
+ +

Über jede Liste kannst du mit foreach Schritt für Schritt gehen:

+ +
foreach ($obst as $frucht) {
+  echo $frucht . "\n";
+}
+ +

Das gibt "Apfel", "Birne", "Kirsche" untereinander aus. Das \n ist ein Zeilenumbruch. Die Variable $frucht bekommt bei jedem Durchlauf den nächsten Wert aus der Liste.

+ +

Listen müssen nicht aus Strings bestehen. Zahlen gehen genauso:

+ +
$zahlen = [10, 20, 30];
+$summe = 0;
+
+foreach ($zahlen as $zahl) {
+  $summe = $summe + $zahl;
+}
+
+echo $summe;                  // 60
+ + +

Funktionen

+ +

Wenn du denselben Code mehrfach brauchst, packst du ihn in eine Funktion. Du gibst der Funktion einen Namen und kannst sie dann beliebig oft aufrufen:

+ +
function begruessen($name) {
+  echo "Hallo, " . $name . "!\n";
+}
+
+begruessen("Marek");
+begruessen("Anna");
+begruessen("Tom");
+ +

Die Funktion begruessen nimmt einen Parameter entgegen ($name). Beim Aufruf übergibst du den konkreten Wert in den Klammern.

+ +

Funktionen können auch Werte zurückgeben. Dafür gibt es return:

+ +
function addiere($a, $b) {
+  return $a + $b;
+}
+
+$ergebnis = addiere(3, 5);
+echo $ergebnis;                // 8
+ +

Die Funktion macht ihre Berechnung und liefert das Ergebnis zurück. Du fängst es in einer Variable auf und kannst damit weiterarbeiten.

+ +
+
+
+ Übung macht den Meister + Schreibe jetzt selbst ein kleines PHP-Programm. Zum Beispiel: eine Liste deiner Lieblings-Filme, die mit foreach ausgegeben werden. Oder eine Funktion, die das Doppelte einer Zahl zurückgibt. Praktisch ausprobieren ist der schnellste Weg, PHP zu lernen. +
+
+ + + +``` \ No newline at end of file diff --git a/templates/Referenz/OnePager.md b/templates/Referenz/OnePager.md new file mode 100644 index 0000000..f315337 --- /dev/null +++ b/templates/Referenz/OnePager.md @@ -0,0 +1,538 @@ +``` + + + + +PHP OnePager + + + +
+ + +
+ +
+

PHP – Server-Sprache des Web

+

Dynamische Webseiten · seit 1995 · 75% aller Websites · objektorientiert & funktional

+
+
+
+
+
8.4
+
Aktuelle Version
+
+
+
1995
+
Erstes Release
+
+
+
75%
+
Aller Websites
+
+
+
300k+
+
Composer-Pakete
+
+
+
+
+ +
+ + +
+ + +
+ +
+

+ + + + Kernkonzepte +

+
+
+
1
+
Server-seitig – läuft auf Webservern, generiert HTML pro Request
+
+
+
2
+
Dynamisch typisiert – Typen zur Laufzeit, optional mit Type-Hints prüfbar
+
+
+
3
+
OOP & funktional – Klassen, Interfaces, Traits, Enums, First-Class-Funktionen
+
+
+
4
+
Composer – moderner Paket-Manager für Dependencies und Autoloading
+
+
+
+ +
+

+ + + + Datentypen +

+
+
intGanze Zahlen
+
floatKommazahlen
+
stringText in '' oder ""
+
booltrue / false
+
arrayListe oder Map
+
objectKlassen-Instanz
+
nullkein Wert
+
callableFunktion / Closure
+
+
+ +
+ + +
+ +
+

+ + + + Hello World +

+
<?php
+declare(strict_types=1);
+
+function greet(string $name): string {
+  return "Hallo, $name!";
+}
+
+echo greet('Marek');
+// Hallo, Marek!
+
+ +
+

+ + + + Moderne vs. Legacy +

+
+
+

Modern (8.x)

+
    +
  • strict_types
  • +
  • Readonly Properties
  • +
  • Enums & Match
  • +
  • Named Arguments
  • +
  • Promoted Constructor
  • +
  • Composer + PSR
  • +
+
+
+

Legacy (5.x)

+
    +
  • magic_quotes
  • +
  • register_globals
  • +
  • mysql_* Funktionen
  • +
  • kein Typ-System
  • +
  • include/require Chaos
  • +
  • Spaghetti-Code
  • +
+
+
+
+ +
+ + +
+ +
+

+ + + + + + Ökosystem +

+
+
+
+
+ Laravel + Full-Stack Framework +
+
+
+
+
+ Symfony + Enterprise-Framework +
+
+
+
+
+ Composer + Paket-Manager +
+
+
+
+
+ PHPUnit + Testing-Framework +
+
+
+
+
+ PHPStan + Static Analysis +
+
+
+
+
+ Shopware + E-Commerce-Plattform +
+
+
+
+ +
+

+ + + + Einsatzgebiete +

+
+
+
W
+
Web-Backends – REST/GraphQL APIs, MVC-Apps mit Laravel oder Symfony
+
+
+
C
+
CMS – WordPress, Drupal, TYPO3 für Content-Management
+
+
+
E
+
E-Commerce – Shopware, Magento, WooCommerce
+
+
+
$
+
CLI-Tools – Symfony Console, Laravel Artisan für Automation
+
+
+
+ +
+ +
+ + + + +
+ + +``` \ No newline at end of file