docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,312 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Performance;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use Psr\Log\LoggerInterface;
/**
* Monitors database queries for N+1 and other performance issues
*/
final class QueryMonitor
{
private array $queryLog = [];
private array $queryPatterns = [];
private int $queryCount = 0;
private float $totalTime = 0.0;
private bool $enabled = true;
/**
* Threshold for detecting N+1 queries
*/
private const N1_THRESHOLD = 3;
/**
* Threshold for slow queries
*/
private readonly Duration $slowQueryThreshold;
public function __construct(
private readonly ?LoggerInterface $logger = null,
?Duration $slowQueryThreshold = null
) {
$this->slowQueryThreshold = $slowQueryThreshold ?? Duration::fromMilliseconds(100);
}
/**
* Logs a query execution
*/
public function logQuery(string $sql, array $params = [], ?Duration $executionTime = null): void
{
if (! $this->enabled) {
return;
}
$executionTime ??= Duration::zero();
$this->queryCount++;
$this->totalTime += $executionTime->toSeconds();
// Normalize query for pattern detection
$pattern = $this->normalizeQuery($sql);
// Track query patterns for N+1 detection
if (! isset($this->queryPatterns[$pattern])) {
$this->queryPatterns[$pattern] = [
'count' => 0,
'total_time' => 0.0,
'first_sql' => $sql,
'params_samples' => [],
];
}
$this->queryPatterns[$pattern]['count']++;
$this->queryPatterns[$pattern]['total_time'] += $executionTime->toSeconds();
// Store sample params for debugging
if (count($this->queryPatterns[$pattern]['params_samples']) < 5) {
$this->queryPatterns[$pattern]['params_samples'][] = $params;
}
// Add to query log
$this->queryLog[] = [
'sql' => $sql,
'params' => $params,
'time' => $executionTime,
'pattern' => $pattern,
'timestamp' => microtime(true),
];
// Check for slow queries
if ($executionTime->greaterThan($this->slowQueryThreshold)) {
$this->handleSlowQuery($sql, $params, $executionTime);
}
// Check for N+1 patterns
if ($this->queryPatterns[$pattern]['count'] >= self::N1_THRESHOLD) {
$this->handlePotentialN1($pattern, $this->queryPatterns[$pattern]);
}
}
/**
* Normalizes a query to detect patterns
*/
private function normalizeQuery(string $sql): string
{
// Remove whitespace variations
$sql = preg_replace('/\s+/', ' ', trim($sql));
// Replace values with placeholders
// Numbers
$sql = preg_replace('/\b\d+\b/', '?', $sql);
// Quoted strings
$sql = preg_replace("/'[^']*'/", '?', $sql);
$sql = preg_replace('/"[^"]*"/', '?', $sql);
// IN clauses with multiple values
$sql = preg_replace('/IN\s*\([^)]+\)/i', 'IN (?)', $sql);
// Remove comments
$sql = preg_replace('/--.*$/', '', $sql);
$sql = preg_replace('/\/\*.*?\*\//', '', $sql);
return strtolower($sql);
}
/**
* Handles detection of slow queries
*/
private function handleSlowQuery(string $sql, array $params, Duration $executionTime): void
{
$message = sprintf(
'Slow query detected (%.2fms): %s',
$executionTime->toMilliseconds(),
$this->truncateSQL($sql)
);
if ($this->logger) {
$this->logger->warning($message, [
'sql' => $sql,
'params' => $params,
'execution_time_ms' => $executionTime->toMilliseconds(),
]);
}
}
/**
* Handles detection of potential N+1 queries
*/
private function handlePotentialN1(string $pattern, array $patternData): void
{
// Only warn once per pattern per request
static $warnedPatterns = [];
if (isset($warnedPatterns[$pattern])) {
return;
}
$warnedPatterns[$pattern] = true;
$message = sprintf(
'Potential N+1 query detected: Pattern executed %d times (%.2fms total)',
$patternData['count'],
$patternData['total_time'] * 1000
);
if ($this->logger) {
$this->logger->warning($message, [
'pattern' => $pattern,
'example_sql' => $patternData['first_sql'],
'count' => $patternData['count'],
'total_time_ms' => $patternData['total_time'] * 1000,
'param_samples' => $patternData['params_samples'],
]);
}
}
/**
* Gets query statistics
*/
public function getStatistics(): QueryStatistics
{
$n1Queries = array_filter($this->queryPatterns, fn ($p) => $p['count'] >= self::N1_THRESHOLD);
$slowQueries = array_filter($this->queryLog, fn ($q) => $q['time']->greaterThan($this->slowQueryThreshold));
return QueryStatistics::fromRawData(
totalQueries: $this->queryCount,
totalTimeSeconds: $this->totalTime,
uniquePatterns: count($this->queryPatterns),
n1QueryPatterns: count($n1Queries),
slowQueries: count($slowQueries),
queryPatterns: $this->queryPatterns,
queryLog: $this->queryLog
);
}
/**
* Resets the monitor
*/
public function reset(): void
{
$this->queryLog = [];
$this->queryPatterns = [];
$this->queryCount = 0;
$this->totalTime = 0.0;
}
/**
* Enables or disables monitoring
*/
public function setEnabled(bool $enabled): void
{
$this->enabled = $enabled;
}
/**
* Checks if monitoring is enabled
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Truncates SQL for display
*/
private function truncateSQL(string $sql, int $maxLength = 100): string
{
if (strlen($sql) <= $maxLength) {
return $sql;
}
return substr($sql, 0, $maxLength) . '...';
}
/**
* Analyzes queries and returns recommendations
*/
public function getRecommendations(): array
{
$recommendations = [];
$stats = $this->getStatistics();
// Check for N+1 queries
if ($stats->n1QueryPatterns > 0) {
$n1Patterns = array_filter($this->queryPatterns, fn ($p) => $p['count'] >= self::N1_THRESHOLD);
foreach ($n1Patterns as $pattern => $data) {
$recommendations[] = [
'type' => 'n1_query',
'severity' => 'high',
'message' => sprintf(
'N+1 Query Pattern: "%s" executed %d times. Consider using batch loading with findWithRelations().',
$this->truncateSQL($data['first_sql']),
$data['count']
),
'impact' => sprintf('%.2fms total time', $data['total_time'] * 1000),
'solution' => 'Use EntityManager::findWithRelations() or BatchLoader to preload relations',
];
}
}
// Check for slow queries
$slowQueries = array_filter($this->queryLog, fn ($q) => $q['time']->greaterThan($this->slowQueryThreshold));
foreach ($slowQueries as $query) {
$recommendations[] = [
'type' => 'slow_query',
'severity' => 'medium',
'message' => sprintf(
'Slow Query: "%s" took %.2fms',
$this->truncateSQL($query['sql']),
$query['time']->toMilliseconds()
),
'impact' => 'Performance degradation',
'solution' => 'Consider adding indexes or optimizing the query',
];
}
// Check for too many queries
if ($stats->totalQueries > 50) {
$recommendations[] = [
'type' => 'too_many_queries',
'severity' => 'medium',
'message' => sprintf('%d queries executed in this request', $stats->totalQueries),
'impact' => sprintf('%.2fms total database time', $stats->totalTime->toMilliseconds()),
'solution' => 'Consider caching or batch loading to reduce query count',
];
}
return $recommendations;
}
/**
* Throws exception if critical issues detected (for dev mode)
*/
public function assertNoPerformanceIssues(): void
{
$stats = $this->getStatistics();
if ($stats->n1QueryPatterns > 0) {
$n1Patterns = array_filter($this->queryPatterns, fn ($p) => $p['count'] >= self::N1_THRESHOLD);
$examples = array_map(fn ($p) => $p['first_sql'], array_slice($n1Patterns, 0, 3));
throw FrameworkException::create(
ErrorCode::DB_PERFORMANCE_ISSUE,
sprintf(
'N+1 Query detected: %d patterns found. Examples: %s',
$stats->n1QueryPatterns,
implode('; ', $examples)
)
);
}
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Performance;
use App\Framework\Core\ValueObjects\Duration;
/**
* Value Object für Query-Statistiken
*/
final readonly class QueryStatistics
{
public function __construct(
public int $totalQueries,
public Duration $totalTime,
public Duration $averageTime,
public int $uniquePatterns,
public int $n1QueryPatterns,
public int $slowQueries,
public array $queryPatterns,
public array $queryLog
) {
}
/**
* Factory method für die Erstellung aus rohen Daten
*/
public static function fromRawData(
int $totalQueries,
float $totalTimeSeconds,
int $uniquePatterns,
int $n1QueryPatterns,
int $slowQueries,
array $queryPatterns,
array $queryLog
): self {
$totalTime = Duration::fromSeconds($totalTimeSeconds);
$averageTime = $totalQueries > 0
? Duration::fromSeconds($totalTimeSeconds / $totalQueries)
: Duration::zero();
return new self(
totalQueries: $totalQueries,
totalTime: $totalTime,
averageTime: $averageTime,
uniquePatterns: $uniquePatterns,
n1QueryPatterns: $n1QueryPatterns,
slowQueries: $slowQueries,
queryPatterns: $queryPatterns,
queryLog: $queryLog
);
}
/**
* Gibt die Statistiken als Array zurück
*/
public function toArray(): array
{
return [
'total_queries' => $this->totalQueries,
'total_time_ms' => $this->totalTime->toMilliseconds(),
'average_time_ms' => $this->averageTime->toMilliseconds(),
'unique_patterns' => $this->uniquePatterns,
'n1_query_patterns' => $this->n1QueryPatterns,
'slow_queries' => $this->slowQueries,
'patterns' => array_map(function ($pattern) {
$patternTotalTime = Duration::fromSeconds($pattern['total_time']);
$patternAverageTime = Duration::fromSeconds($pattern['total_time'] / $pattern['count']);
return [
'count' => $pattern['count'],
'total_time_ms' => $patternTotalTime->toMilliseconds(),
'average_time_ms' => $patternAverageTime->toMilliseconds(),
'example' => $pattern['first_sql'] ?? '',
];
}, array_slice($this->queryPatterns, 0, 10)), // Top 10 patterns
];
}
/**
* Prüft ob Performance-Probleme vorliegen
*/
public function hasPerformanceIssues(): bool
{
return $this->n1QueryPatterns > 0 ||
$this->slowQueries > 0 ||
$this->totalQueries > 100;
}
/**
* Gibt eine lesbare Zusammenfassung zurück
*/
public function getSummary(): string
{
$summary = sprintf(
"Queries: %d | Time: %.2fms | Avg: %.2fms",
$this->totalQueries,
$this->totalTime->toMilliseconds(),
$this->averageTime->toMilliseconds()
);
if ($this->n1QueryPatterns > 0) {
$summary .= sprintf(" | N+1: %d patterns", $this->n1QueryPatterns);
}
if ($this->slowQueries > 0) {
$summary .= sprintf(" | Slow: %d", $this->slowQueries);
}
return $summary;
}
/**
* Prüft ob die Gesamtzeit ein bestimmtes Limit überschreitet
*/
public function exceedsTimeLimit(Duration $limit): bool
{
return $this->totalTime->greaterThan($limit);
}
/**
* Prüft ob die durchschnittliche Query-Zeit ein Limit überschreitet
*/
public function averageExceedsLimit(Duration $limit): bool
{
return $this->averageTime->greaterThan($limit);
}
}