{
"event": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "\/var\/www\/tools\/ki-protokoll\/claude-hook\/quality\/phpmetrics_check.py",
"content": "#!\/usr\/bin\/env python3\n\"\"\"\nPHPMetrics Integration für Cohesion\/Coupling-Analyse.\n\nLäuft PHPMetrics auf der Codebase und erzeugt Warnungen\nfür Klassen mit schlechten Metriken.\n\nPrinzip: Cohesion over Coupling (#13)\n\"\"\"\n\nimport json\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple\n\n# =============================================================================\n# KONFIGURATION\n# =============================================================================\n\nPHPMETRICS_BIN = \"\/opt\/php-tools\/vendor\/bin\/phpmetrics\"\nSRC_PATH = \"\/var\/www\/dev.campus.systemische-tools.de\/src\"\nCACHE_FILE = \"\/tmp\/phpmetrics_cache.json\"\nCACHE_MAX_AGE_SECONDS = 3600 # 1 Stunde\n\n# Schwellwerte für Warnungen\nTHRESHOLDS = {\n \"lcom\": 4, # Lack of Cohesion - höher = schlechter\n \"afferent_coupling\": 10, # Wie viele Klassen nutzen diese\n \"efferent_coupling\": 15, # Wie viele Klassen nutzt diese\n \"instability_low\": 0.2, # Zu stabil (schwer zu ändern)\n \"instability_high\": 0.8, # Zu instabil (zu viele Abhängigkeiten)\n \"ccn_method_max\": 15, # Maximale Komplexität pro Methode\n \"wmc\": 50, # Weighted Methods per Class\n}\n\n# Pfade die ignoriert werden\nSKIP_PATHS = [\n \"\/vendor\/\",\n \"\/tests\/\",\n \"\/Test\/\",\n]\n\n\n# =============================================================================\n# HELPER\n# =============================================================================\n\ndef is_cache_valid() -> bool:\n \"\"\"Prüft ob der Cache noch gültig ist.\"\"\"\n cache = Path(CACHE_FILE)\n if not cache.exists():\n return False\n import time\n age = time.time() - cache.stat().st_mtime\n return age < CACHE_MAX_AGE_SECONDS\n\n\ndef load_metrics() -> Dict:\n \"\"\"Lädt Metriken aus Cache oder führt PHPMetrics aus.\"\"\"\n if is_cache_valid():\n with open(CACHE_FILE) as f:\n return json.load(f)\n\n # PHPMetrics ausführen\n result = subprocess.run(\n [PHPMETRICS_BIN, \"--report-json=\" + CACHE_FILE, SRC_PATH],\n capture_output=True,\n text=True,\n timeout=300\n )\n\n if result.returncode != 0 and not Path(CACHE_FILE).exists():\n return {}\n\n with open(CACHE_FILE) as f:\n return json.load(f)\n\n\ndef should_skip(class_name: str) -> bool:\n \"\"\"Prüft ob die Klasse übersprungen werden soll.\"\"\"\n return any(skip in class_name for skip in SKIP_PATHS)\n\n\n# =============================================================================\n# ANALYSE\n# =============================================================================\n\ndef analyze_class(name: str, data: Dict) -> List[str]:\n \"\"\"Analysiert eine Klasse und gibt Warnungen zurück.\"\"\"\n warnings = []\n\n if not isinstance(data, dict):\n return warnings\n\n # LCOM - Lack of Cohesion of Methods\n lcom = data.get(\"lcom\", 0)\n if lcom > THRESHOLDS[\"lcom\"]:\n warnings.append(\n f\"W13.1: {name} has low cohesion (LCOM={lcom}). \"\n \"Consider splitting into focused classes.\"\n )\n\n # Afferent Coupling (incoming dependencies)\n afferent = data.get(\"afferentCoupling\", 0)\n if afferent > THRESHOLDS[\"afferent_coupling\"]:\n warnings.append(\n f\"W13.2: {name} has high afferent coupling ({afferent} dependents). \"\n \"Changes here affect many classes - consider interface.\"\n )\n\n # Efferent Coupling (outgoing dependencies)\n efferent = data.get(\"efferentCoupling\", 0)\n if efferent > THRESHOLDS[\"efferent_coupling\"]:\n warnings.append(\n f\"W13.3: {name} has high efferent coupling ({efferent} dependencies). \"\n \"Consider dependency injection or facade pattern.\"\n )\n\n # Instability\n instability = data.get(\"instability\", 0.5)\n if instability < THRESHOLDS[\"instability_low\"] and afferent > 5:\n warnings.append(\n f\"W13.4: {name} is very stable (I={instability:.2f}) with many dependents. \"\n \"Changes are risky - ensure good test coverage.\"\n )\n\n # Cyclomatic Complexity per Method\n ccn_max = data.get(\"ccnMethodMax\", 0)\n if ccn_max > THRESHOLDS[\"ccn_method_max\"]:\n warnings.append(\n f\"W13.5: {name} has high method complexity (CCN={ccn_max}). \"\n \"Consider extracting methods or simplifying logic.\"\n )\n\n # Weighted Methods per Class\n wmc = data.get(\"wmc\", 0)\n if wmc > THRESHOLDS[\"wmc\"]:\n warnings.append(\n f\"W13.6: {name} has high overall complexity (WMC={wmc}). \"\n \"Class is doing too much - consider splitting.\"\n )\n\n return warnings\n\n\ndef run_analysis() -> Tuple[List[str], Dict]:\n \"\"\"Führt die vollständige Analyse durch.\"\"\"\n metrics = load_metrics()\n all_warnings = []\n stats = {\n \"classes_analyzed\": 0,\n \"classes_with_warnings\": 0,\n \"total_warnings\": 0,\n }\n\n for name, data in metrics.items():\n if should_skip(name):\n continue\n\n if not isinstance(data, dict):\n continue\n\n stats[\"classes_analyzed\"] += 1\n warnings = analyze_class(name, data)\n\n if warnings:\n stats[\"classes_with_warnings\"] += 1\n stats[\"total_warnings\"] += len(warnings)\n all_warnings.extend(warnings)\n\n return all_warnings, stats\n\n\n# =============================================================================\n# MAIN\n# =============================================================================\n\ndef main():\n \"\"\"Hauptfunktion für CLI-Aufruf.\"\"\"\n import argparse\n\n parser = argparse.ArgumentParser(description=\"PHPMetrics Cohesion Check\")\n parser.add_argument(\"--refresh\", action=\"store_true\", help=\"Cache neu erstellen\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"JSON-Ausgabe\")\n parser.add_argument(\"--class\", dest=\"class_name\", help=\"Nur eine Klasse prüfen\")\n args = parser.parse_args()\n\n if args.refresh and Path(CACHE_FILE).exists():\n Path(CACHE_FILE).unlink()\n\n warnings, stats = run_analysis()\n\n if args.json:\n print(json.dumps({\n \"warnings\": warnings,\n \"stats\": stats\n }, indent=2))\n else:\n print(f\"PHPMetrics Analysis: {stats['classes_analyzed']} classes\")\n print(f\"Classes with warnings: {stats['classes_with_warnings']}\")\n print(f\"Total warnings: {stats['total_warnings']}\")\n print()\n\n if warnings:\n print(\"Warnings:\")\n for w in warnings[:20]: # Limit output\n print(f\" - {w}\")\n\n if len(warnings) > 20:\n print(f\" ... and {len(warnings) - 20} more\")\n\n\nif __name__ == \"__main__\":\n main()\n"
}
}
{
"tool_response": {
"type": "create",
"filePath": "\/var\/www\/tools\/ki-protokoll\/claude-hook\/quality\/phpmetrics_check.py",
"content": "#!\/usr\/bin\/env python3\n\"\"\"\nPHPMetrics Integration für Cohesion\/Coupling-Analyse.\n\nLäuft PHPMetrics auf der Codebase und erzeugt Warnungen\nfür Klassen mit schlechten Metriken.\n\nPrinzip: Cohesion over Coupling (#13)\n\"\"\"\n\nimport json\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple\n\n# =============================================================================\n# KONFIGURATION\n# =============================================================================\n\nPHPMETRICS_BIN = \"\/opt\/php-tools\/vendor\/bin\/phpmetrics\"\nSRC_PATH = \"\/var\/www\/dev.campus.systemische-tools.de\/src\"\nCACHE_FILE = \"\/tmp\/phpmetrics_cache.json\"\nCACHE_MAX_AGE_SECONDS = 3600 # 1 Stunde\n\n# Schwellwerte für Warnungen\nTHRESHOLDS = {\n \"lcom\": 4, # Lack of Cohesion - höher = schlechter\n \"afferent_coupling\": 10, # Wie viele Klassen nutzen diese\n \"efferent_coupling\": 15, # Wie viele Klassen nutzt diese\n \"instability_low\": 0.2, # Zu stabil (schwer zu ändern)\n \"instability_high\": 0.8, # Zu instabil (zu viele Abhängigkeiten)\n \"ccn_method_max\": 15, # Maximale Komplexität pro Methode\n \"wmc\": 50, # Weighted Methods per Class\n}\n\n# Pfade die ignoriert werden\nSKIP_PATHS = [\n \"\/vendor\/\",\n \"\/tests\/\",\n \"\/Test\/\",\n]\n\n\n# =============================================================================\n# HELPER\n# =============================================================================\n\ndef is_cache_valid() -> bool:\n \"\"\"Prüft ob der Cache noch gültig ist.\"\"\"\n cache = Path(CACHE_FILE)\n if not cache.exists():\n return False\n import time\n age = time.time() - cache.stat().st_mtime\n return age < CACHE_MAX_AGE_SECONDS\n\n\ndef load_metrics() -> Dict:\n \"\"\"Lädt Metriken aus Cache oder führt PHPMetrics aus.\"\"\"\n if is_cache_valid():\n with open(CACHE_FILE) as f:\n return json.load(f)\n\n # PHPMetrics ausführen\n result = subprocess.run(\n [PHPMETRICS_BIN, \"--report-json=\" + CACHE_FILE, SRC_PATH],\n capture_output=True,\n text=True,\n timeout=300\n )\n\n if result.returncode != 0 and not Path(CACHE_FILE).exists():\n return {}\n\n with open(CACHE_FILE) as f:\n return json.load(f)\n\n\ndef should_skip(class_name: str) -> bool:\n \"\"\"Prüft ob die Klasse übersprungen werden soll.\"\"\"\n return any(skip in class_name for skip in SKIP_PATHS)\n\n\n# =============================================================================\n# ANALYSE\n# =============================================================================\n\ndef analyze_class(name: str, data: Dict) -> List[str]:\n \"\"\"Analysiert eine Klasse und gibt Warnungen zurück.\"\"\"\n warnings = []\n\n if not isinstance(data, dict):\n return warnings\n\n # LCOM - Lack of Cohesion of Methods\n lcom = data.get(\"lcom\", 0)\n if lcom > THRESHOLDS[\"lcom\"]:\n warnings.append(\n f\"W13.1: {name} has low cohesion (LCOM={lcom}). \"\n \"Consider splitting into focused classes.\"\n )\n\n # Afferent Coupling (incoming dependencies)\n afferent = data.get(\"afferentCoupling\", 0)\n if afferent > THRESHOLDS[\"afferent_coupling\"]:\n warnings.append(\n f\"W13.2: {name} has high afferent coupling ({afferent} dependents). \"\n \"Changes here affect many classes - consider interface.\"\n )\n\n # Efferent Coupling (outgoing dependencies)\n efferent = data.get(\"efferentCoupling\", 0)\n if efferent > THRESHOLDS[\"efferent_coupling\"]:\n warnings.append(\n f\"W13.3: {name} has high efferent coupling ({efferent} dependencies). \"\n \"Consider dependency injection or facade pattern.\"\n )\n\n # Instability\n instability = data.get(\"instability\", 0.5)\n if instability < THRESHOLDS[\"instability_low\"] and afferent > 5:\n warnings.append(\n f\"W13.4: {name} is very stable (I={instability:.2f}) with many dependents. \"\n \"Changes are risky - ensure good test coverage.\"\n )\n\n # Cyclomatic Complexity per Method\n ccn_max = data.get(\"ccnMethodMax\", 0)\n if ccn_max > THRESHOLDS[\"ccn_method_max\"]:\n warnings.append(\n f\"W13.5: {name} has high method complexity (CCN={ccn_max}). \"\n \"Consider extracting methods or simplifying logic.\"\n )\n\n # Weighted Methods per Class\n wmc = data.get(\"wmc\", 0)\n if wmc > THRESHOLDS[\"wmc\"]:\n warnings.append(\n f\"W13.6: {name} has high overall complexity (WMC={wmc}). \"\n \"Class is doing too much - consider splitting.\"\n )\n\n return warnings\n\n\ndef run_analysis() -> Tuple[List[str], Dict]:\n \"\"\"Führt die vollständige Analyse durch.\"\"\"\n metrics = load_metrics()\n all_warnings = []\n stats = {\n \"classes_analyzed\": 0,\n \"classes_with_warnings\": 0,\n \"total_warnings\": 0,\n }\n\n for name, data in metrics.items():\n if should_skip(name):\n continue\n\n if not isinstance(data, dict):\n continue\n\n stats[\"classes_analyzed\"] += 1\n warnings = analyze_class(name, data)\n\n if warnings:\n stats[\"classes_with_warnings\"] += 1\n stats[\"total_warnings\"] += len(warnings)\n all_warnings.extend(warnings)\n\n return all_warnings, stats\n\n\n# =============================================================================\n# MAIN\n# =============================================================================\n\ndef main():\n \"\"\"Hauptfunktion für CLI-Aufruf.\"\"\"\n import argparse\n\n parser = argparse.ArgumentParser(description=\"PHPMetrics Cohesion Check\")\n parser.add_argument(\"--refresh\", action=\"store_true\", help=\"Cache neu erstellen\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"JSON-Ausgabe\")\n parser.add_argument(\"--class\", dest=\"class_name\", help=\"Nur eine Klasse prüfen\")\n args = parser.parse_args()\n\n if args.refresh and Path(CACHE_FILE).exists():\n Path(CACHE_FILE).unlink()\n\n warnings, stats = run_analysis()\n\n if args.json:\n print(json.dumps({\n \"warnings\": warnings,\n \"stats\": stats\n }, indent=2))\n else:\n print(f\"PHPMetrics Analysis: {stats['classes_analyzed']} classes\")\n print(f\"Classes with warnings: {stats['classes_with_warnings']}\")\n print(f\"Total warnings: {stats['total_warnings']}\")\n print()\n\n if warnings:\n print(\"Warnings:\")\n for w in warnings[:20]: # Limit output\n print(f\" - {w}\")\n\n if len(warnings) > 20:\n print(f\" ... and {len(warnings) - 20} more\")\n\n\nif __name__ == \"__main__\":\n main()\n",
"structuredPatch": [],
"originalFile": null
}
}