Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,253 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Analyzer;
/**
* Ergebnis der Farbanalyse
*/
final readonly class ColorAnalysisResult
{
public function __construct(
public int $totalColors,
public array $colorsByFormat,
public array $colorPalette,
public array $contrastAnalysis,
public array $duplicateColors,
public array $recommendations
) {
}
/**
* Gibt Farbdiversität-Score zurück (0-100)
*/
public function getDiversityScore(): int
{
if ($this->totalColors === 0) {
return 0;
}
$categoryDistribution = [
'primary' => count($this->colorPalette['primary_colors']),
'neutral' => count($this->colorPalette['neutral_colors']),
'accent' => count($this->colorPalette['accent_colors']),
'semantic' => count($this->colorPalette['semantic_colors']),
];
$nonZeroCategories = count(array_filter($categoryDistribution));
$maxCategories = 4;
// Basis-Score für Kategorien-Abdeckung
$categoryScore = ($nonZeroCategories / $maxCategories) * 50;
// Bonus für ausgewogene Verteilung
$total = array_sum($categoryDistribution);
if ($total > 0) {
$entropy = 0;
foreach ($categoryDistribution as $count) {
if ($count > 0) {
$probability = $count / $total;
$entropy -= $probability * log($probability, 2);
}
}
$maxEntropy = log($nonZeroCategories, 2);
$balanceScore = $maxEntropy > 0 ? ($entropy / $maxEntropy) * 50 : 0;
} else {
$balanceScore = 0;
}
return min(100, (int) round($categoryScore + $balanceScore));
}
/**
* Gibt Konsistenz-Score zurück (0-100)
*/
public function getConsistencyScore(): int
{
if ($this->totalColors === 0) {
return 100;
}
$score = 100;
// Abzug für Duplikate
$totalDuplicates = array_sum(array_column($this->duplicateColors, 'potential_savings'));
$duplicatePercentage = ($totalDuplicates / max(1, $this->totalColors)) * 100;
$score -= min(30, $duplicatePercentage * 2);
// Abzug für zu viele Formate
$formatCount = count($this->colorsByFormat);
if ($formatCount > 3) {
$score -= ($formatCount - 3) * 10;
}
return max(0, (int) $score);
}
/**
* Gibt Kontrast-Compliance-Score zurück (0-100)
*/
public function getContrastComplianceScore(): int
{
if (empty($this->contrastAnalysis)) {
return 100; // Kein Kontrast zu prüfen
}
$totalPairs = count($this->contrastAnalysis);
$wcagAAPassing = count(array_filter($this->contrastAnalysis, fn ($c) => $c['wcag_aa']));
return (int) round(($wcagAAPassing / $totalPairs) * 100);
}
/**
* Gibt die häufigsten Farbformate zurück
*/
public function getMostUsedFormats(): array
{
$formatUsage = [];
foreach ($this->colorsByFormat as $format => $colors) {
$formatUsage[$format] = count($colors);
}
arsort($formatUsage);
return $formatUsage;
}
/**
* Gibt Farbpaletten-Zusammenfassung zurück
*/
public function getPaletteSummary(): array
{
return [
'total_colors' => $this->totalColors,
'primary_colors' => count($this->colorPalette['primary_colors']),
'neutral_colors' => count($this->colorPalette['neutral_colors']),
'accent_colors' => count($this->colorPalette['accent_colors']),
'semantic_colors' => count($this->colorPalette['semantic_colors']),
'diversity_score' => $this->getDiversityScore(),
'consistency_score' => $this->getConsistencyScore(),
];
}
/**
* Gibt die problematischsten Farbkombinationen zurück
*/
public function getWorstContrastPairs(): array
{
$lowContrastPairs = array_filter(
$this->contrastAnalysis,
fn ($contrast) => ! $contrast['wcag_aa']
);
// Sortiere nach niedrigstem Kontrast
usort($lowContrastPairs, fn ($a, $b) => $a['contrast_ratio'] <=> $b['contrast_ratio']);
return array_slice($lowContrastPairs, 0, 5); // Top 5 problematischste
}
/**
* Gibt priorisierte Verbesserungs-Aktionen zurück
*/
public function getPrioritizedActions(): array
{
$actions = [];
// Hohe Priorität: Accessibility-Probleme
$badContrastPairs = $this->getWorstContrastPairs();
if (! empty($badContrastPairs)) {
$actions[] = [
'priority' => 'high',
'category' => 'accessibility',
'action' => 'Fix ' . count($badContrastPairs) . ' low-contrast color combinations',
'impact' => 'WCAG compliance and readability',
];
}
// Mittlere Priorität: Duplikate
if (! empty($this->duplicateColors)) {
$totalDuplicates = array_sum(array_column($this->duplicateColors, 'potential_savings'));
$actions[] = [
'priority' => 'medium',
'category' => 'optimization',
'action' => "Consolidate {$totalDuplicates} duplicate colors",
'impact' => 'CSS size reduction and maintenance',
];
}
// Niedrige Priorität: Palette-Optimierung
if ($this->getDiversityScore() < 60) {
$actions[] = [
'priority' => 'low',
'category' => 'design_system',
'action' => 'Improve color palette diversity and semantic naming',
'impact' => 'Design system consistency',
];
}
return $actions;
}
/**
* Gibt Format-Empfehlungen zurück
*/
public function getFormatRecommendations(): array
{
$recommendations = [];
$formats = $this->getMostUsedFormats();
// Zu viele Formate
if (count($formats) > 3) {
$recommendations[] = [
'type' => 'standardization',
'message' => 'Multiple color formats detected. Consider standardizing on 2-3 formats.',
'suggestion' => 'Use hex for static colors, custom properties for theme colors, OKLCH for advanced color spaces.',
];
}
// OKLCH Empfehlung für größere Paletten
if (! isset($formats['oklch']) && $this->totalColors > 15) {
$recommendations[] = [
'type' => 'modern_format',
'message' => 'Consider OKLCH format for better color consistency and interpolation.',
'suggestion' => 'OKLCH provides more perceptually uniform color space, especially useful for generated color scales.',
];
}
// Custom Properties Empfehlung
if (! isset($formats['custom_property']) && $this->totalColors > 8) {
$recommendations[] = [
'type' => 'design_tokens',
'message' => 'Consider using CSS custom properties for theme colors.',
'suggestion' => 'Custom properties enable theming, better maintainability, and runtime color changes.',
];
}
return $recommendations;
}
/**
* Konvertiert zu Array für Export
*/
public function toArray(): array
{
return [
'total_colors' => $this->totalColors,
'colors_by_format' => $this->colorsByFormat,
'color_palette' => $this->colorPalette,
'contrast_analysis' => $this->contrastAnalysis,
'duplicate_colors' => $this->duplicateColors,
'recommendations' => $this->recommendations,
'diversity_score' => $this->getDiversityScore(),
'consistency_score' => $this->getConsistencyScore(),
'contrast_compliance_score' => $this->getContrastComplianceScore(),
'palette_summary' => $this->getPaletteSummary(),
'worst_contrast_pairs' => $this->getWorstContrastPairs(),
'prioritized_actions' => $this->getPrioritizedActions(),
'format_recommendations' => $this->getFormatRecommendations(),
'most_used_formats' => $this->getMostUsedFormats(),
];
}
}

View File

@@ -0,0 +1,610 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Analyzer;
use App\Framework\Core\ValueObjects\RGBColor;
use App\Framework\Design\ValueObjects\CssColor;
use App\Framework\Design\ValueObjects\CssParseResult;
use App\Framework\Design\ValueObjects\CssPropertyCategory;
/**
* Analysiert Farben im Design System
*/
final readonly class ColorAnalyzer
{
/**
* Analysiert alle Farben im CSS
*/
public function analyzeColors(CssParseResult $parseResult): ColorAnalysisResult
{
// Sammle alle Farben
$colors = $this->collectAllColors($parseResult);
if (empty($colors)) {
return new ColorAnalysisResult(
totalColors: 0,
colorsByFormat: [],
colorPalette: [],
contrastAnalysis: [],
duplicateColors: [],
recommendations: ['No colors found in CSS. Consider implementing a color system with design tokens.']
);
}
// Analysiere Farbpalette
$palette = $this->generateColorPalette($colors);
// Gruppiere nach Format
$colorsByFormat = $this->groupColorsByFormat($colors);
// Analysiere Kontraste (vereinfacht)
$contrastAnalysis = $this->analyzeContrasts($colors);
// Finde Duplikate
$duplicates = $this->findDuplicateColors($colors);
// Generiere Empfehlungen
$recommendations = $this->generateRecommendations($colors, $palette, $duplicates, $colorsByFormat);
return new ColorAnalysisResult(
totalColors: count($colors),
colorsByFormat: $colorsByFormat,
colorPalette: $palette,
contrastAnalysis: $contrastAnalysis,
duplicateColors: $duplicates,
recommendations: $recommendations
);
}
/**
* Analysiert spezifische Farbharmonien
*/
public function analyzeColorHarmony(array $colors): array
{
$harmony = [
'monochromatic' => [],
'complementary' => [],
'triadic' => [],
'analogous' => [],
'split_complementary' => [],
];
foreach ($colors as $color) {
$rgb = $color['color']->toRGB();
if (! $rgb) {
continue;
}
$hsl = $this->rgbToHsl($rgb);
$hue = $hsl['h'];
// Finde harmonische Farben
foreach ($colors as $otherColor) {
if ($color === $otherColor) {
continue;
}
$otherRgb = $otherColor['color']->toRGB();
if (! $otherRgb) {
continue;
}
$otherHsl = $this->rgbToHsl($otherRgb);
$otherHue = $otherHsl['h'];
$hueDiff = abs($hue - $otherHue);
$hueDiff = min($hueDiff, 360 - $hueDiff); // Zirkuläre Distanz
// Klassifiziere Harmonie-Typ
if ($hueDiff < 30) {
$harmony['analogous'][] = [$color, $otherColor];
} elseif ($hueDiff > 150 && $hueDiff < 210) {
$harmony['complementary'][] = [$color, $otherColor];
} elseif (abs($hueDiff - 120) < 30 || abs($hueDiff - 240) < 30) {
$harmony['triadic'][] = [$color, $otherColor];
}
}
}
return $harmony;
}
/**
* Bewertet Farbzugänglichkeit
*/
public function analyzeAccessibility(CssParseResult $parseResult): array
{
$accessibilityIssues = [];
$colorProperties = $parseResult->getPropertiesByCategory(CssPropertyCategory::COLOR);
// Sammle Text/Background-Kombinationen
$textBackgroundPairs = [];
foreach ($parseResult->rules as $rule) {
$textColor = null;
$backgroundColor = null;
foreach ($rule->properties as $property) {
if ($property->name === 'color') {
$textColor = $property->toColor();
} elseif ($property->name === 'background-color') {
$backgroundColor = $property->toColor();
}
}
if ($textColor && $backgroundColor) {
$contrast = $this->calculateContrast($textColor, $backgroundColor);
if ($contrast < 4.5) { // WCAG AA Standard
$accessibilityIssues[] = [
'type' => 'low_contrast',
'severity' => $contrast < 3.0 ? 'high' : 'medium',
'contrast_ratio' => round($contrast, 2),
'text_color' => $textColor->originalValue,
'background_color' => $backgroundColor->originalValue,
'selectors' => $rule->getSelectorStrings(),
];
}
}
}
// Analysiere reine Farbkommunikation
$colorOnlyIndicators = $this->findColorOnlyIndicators($parseResult);
return [
'contrast_issues' => $accessibilityIssues,
'color_only_indicators' => $colorOnlyIndicators,
'total_issues' => count($accessibilityIssues) + count($colorOnlyIndicators),
'accessibility_score' => $this->calculateAccessibilityScore($accessibilityIssues, $colorOnlyIndicators),
];
}
/**
* Sammelt alle Farben aus dem CSS
*/
private function collectAllColors(CssParseResult $parseResult): array
{
$colors = [];
// Farben aus Properties
$colorProperties = $parseResult->getPropertiesByCategory(CssPropertyCategory::COLOR);
foreach ($colorProperties as $property) {
$color = $property->toColor();
if ($color) {
$colors[] = [
'color' => $color,
'property' => $property->name,
'context' => 'property',
'usage_count' => 1,
];
}
}
// Farben aus Design Tokens
foreach ($parseResult->customProperties as $token) {
if ($token->hasValueType('color')) {
$color = $token->getValueAs('color');
if ($color instanceof CssColor) {
$colors[] = [
'color' => $color,
'property' => $token->name,
'context' => 'token',
'usage_count' => 0, // TODO: Track usage
];
}
}
}
return $colors;
}
/**
* Generiert eine strukturierte Farbpalette
*/
private function generateColorPalette(array $colors): array
{
$palette = [
'primary_colors' => [],
'neutral_colors' => [],
'accent_colors' => [],
'semantic_colors' => [],
];
foreach ($colors as $colorData) {
$color = $colorData['color'];
$property = $colorData['property'];
// Kategorisiere Farben basierend auf Namen und Eigenschaften
if ($this->isPrimaryColor($property, $color)) {
$palette['primary_colors'][] = $colorData;
} elseif ($this->isNeutralColor($property, $color)) {
$palette['neutral_colors'][] = $colorData;
} elseif ($this->isSemanticColor($property)) {
$palette['semantic_colors'][] = $colorData;
} else {
$palette['accent_colors'][] = $colorData;
}
}
// Sortiere jede Kategorie nach Helligkeit
foreach ($palette as $category => &$categoryColors) {
usort($categoryColors, function ($a, $b) {
$rgbA = $a['color']->toRGB();
$rgbB = $b['color']->toRGB();
if (! $rgbA || ! $rgbB) {
return 0;
}
$lightnessA = $this->calculateLightness($rgbA);
$lightnessB = $this->calculateLightness($rgbB);
return $lightnessA <=> $lightnessB;
});
}
return $palette;
}
/**
* Gruppiert Farben nach Format
*/
private function groupColorsByFormat(array $colors): array
{
$grouped = [];
foreach ($colors as $colorData) {
$format = $colorData['color']->format->value;
if (! isset($grouped[$format])) {
$grouped[$format] = [];
}
$grouped[$format][] = $colorData;
}
// Sortiere nach Häufigkeit
uasort($grouped, fn ($a, $b) => count($b) <=> count($a));
return $grouped;
}
/**
* Analysiert Farbkontraste
*/
private function analyzeContrasts(array $colors): array
{
$contrasts = [];
// Erstelle Kontrast-Matrix für häufige Kombinationen
foreach ($colors as $i => $colorA) {
foreach ($colors as $j => $colorB) {
if ($i >= $j) {
continue;
}
$contrast = $this->calculateContrast($colorA['color'], $colorB['color']);
if ($contrast !== null) {
$contrasts[] = [
'color_a' => $colorA,
'color_b' => $colorB,
'contrast_ratio' => round($contrast, 2),
'wcag_aa' => $contrast >= 4.5,
'wcag_aaa' => $contrast >= 7.0,
];
}
}
}
// Sortiere nach Kontrast-Ratio
usort($contrasts, fn ($a, $b) => $b['contrast_ratio'] <=> $a['contrast_ratio']);
return array_slice($contrasts, 0, 20); // Limitiere auf Top 20
}
/**
* Findet doppelte Farben
*/
private function findDuplicateColors(array $colors): array
{
$duplicates = [];
$rgbMap = [];
foreach ($colors as $colorData) {
$rgb = $colorData['color']->toRGB();
if (! $rgb) {
continue;
}
$rgbKey = $rgb->red . ',' . $rgb->green . ',' . $rgb->blue;
if (! isset($rgbMap[$rgbKey])) {
$rgbMap[$rgbKey] = [];
}
$rgbMap[$rgbKey][] = $colorData;
}
// Finde Gruppen mit mehr als einem Eintrag
foreach ($rgbMap as $rgbKey => $colorGroup) {
if (count($colorGroup) > 1) {
$duplicates[] = [
'rgb_values' => $rgbKey,
'colors' => $colorGroup,
'count' => count($colorGroup),
'potential_savings' => count($colorGroup) - 1,
];
}
}
// Sortiere nach Anzahl der Duplikate
usort($duplicates, fn ($a, $b) => $b['count'] <=> $a['count']);
return $duplicates;
}
/**
* Berechnet Kontrast zwischen zwei Farben
*/
private function calculateContrast(CssColor $colorA, CssColor $colorB): ?float
{
$rgbA = $colorA->toRGB();
$rgbB = $colorB->toRGB();
if (! $rgbA || ! $rgbB) {
return null;
}
$luminanceA = $this->calculateRelativeLuminance($rgbA);
$luminanceB = $this->calculateRelativeLuminance($rgbB);
$lighter = max($luminanceA, $luminanceB);
$darker = min($luminanceA, $luminanceB);
return ($lighter + 0.05) / ($darker + 0.05);
}
/**
* Berechnet relative Luminanz
*/
private function calculateRelativeLuminance(RGBColor $rgb): float
{
$r = $this->linearizeColorComponent($rgb->red / 255);
$g = $this->linearizeColorComponent($rgb->green / 255);
$b = $this->linearizeColorComponent($rgb->blue / 255);
return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
}
/**
* Linearisiert Farbkomponente für Luminanz-Berechnung
*/
private function linearizeColorComponent(float $component): float
{
if ($component <= 0.03928) {
return $component / 12.92;
} else {
return pow(($component + 0.055) / 1.055, 2.4);
}
}
/**
* Konvertiert RGB zu HSL
*/
private function rgbToHsl(RGBColor $rgb): array
{
$r = $rgb->red / 255;
$g = $rgb->green / 255;
$b = $rgb->blue / 255;
$max = max($r, $g, $b);
$min = min($r, $g, $b);
$diff = $max - $min;
// Lightness
$l = ($max + $min) / 2;
if ($diff === 0.0) {
$h = $s = 0; // Achromatic
} else {
// Saturation
$s = $l > 0.5 ? $diff / (2 - $max - $min) : $diff / ($max + $min);
// Hue
switch ($max) {
case $r:
$h = (($g - $b) / $diff + ($g < $b ? 6 : 0)) / 6;
break;
case $g:
$h = (($b - $r) / $diff + 2) / 6;
break;
case $b:
$h = (($r - $g) / $diff + 4) / 6;
break;
default:
$h = 0;
}
}
return [
'h' => $h * 360,
's' => $s * 100,
'l' => $l * 100,
];
}
/**
* Berechnet Helligkeit einer Farbe
*/
private function calculateLightness(RGBColor $rgb): float
{
return ($rgb->red * 0.299 + $rgb->green * 0.587 + $rgb->blue * 0.114) / 255;
}
/**
* Prüft ob es eine primäre Farbe ist
*/
private function isPrimaryColor(string $property, CssColor $color): bool
{
return str_contains(strtolower($property), 'primary') ||
str_contains(strtolower($property), 'brand') ||
str_contains(strtolower($property), 'main');
}
/**
* Prüft ob es eine neutrale Farbe ist
*/
private function isNeutralColor(string $property, CssColor $color): bool
{
if (str_contains(strtolower($property), 'gray') ||
str_contains(strtolower($property), 'grey') ||
str_contains(strtolower($property), 'neutral')) {
return true;
}
// Prüfe RGB-Werte für Grautöne
$rgb = $color->toRGB();
if ($rgb) {
$maxDiff = max($rgb->red, $rgb->green, $rgb->blue) - min($rgb->red, $rgb->green, $rgb->blue);
return $maxDiff < 20; // Sehr niedrige Sättigung
}
return false;
}
/**
* Prüft ob es eine semantische Farbe ist
*/
private function isSemanticColor(string $property): bool
{
$semanticKeywords = ['success', 'error', 'warning', 'info', 'danger', 'alert'];
foreach ($semanticKeywords as $keyword) {
if (str_contains(strtolower($property), $keyword)) {
return true;
}
}
return false;
}
/**
* Findet reine Farbindikatoren (Accessibility Problem)
*/
private function findColorOnlyIndicators(CssParseResult $parseResult): array
{
$indicators = [];
// Suche nach Patterns die nur Farbe für Information nutzen
foreach ($parseResult->rules as $rule) {
$hasColorChange = false;
$hasOtherIndicator = false;
foreach ($rule->properties as $property) {
if ($property->getCategory() === CssPropertyCategory::COLOR) {
$hasColorChange = true;
}
if (in_array($property->name, ['border', 'text-decoration', 'font-weight', 'background-image'])) {
$hasOtherIndicator = true;
}
}
if ($hasColorChange && ! $hasOtherIndicator) {
// Prüfe ob es sich um Status-Indikatoren handelt
foreach ($rule->selectors as $selector) {
if (preg_match('/\.(error|success|warning|info|active|selected|invalid)/', $selector->value)) {
$indicators[] = [
'selector' => $selector->value,
'issue' => 'Uses color only for status indication',
'suggestion' => 'Add icons, borders, or text indicators alongside color',
];
}
}
}
}
return $indicators;
}
/**
* Berechnet Accessibility Score
*/
private function calculateAccessibilityScore(array $contrastIssues, array $colorOnlyIndicators): int
{
$baseScore = 100;
// Abzug für Kontrast-Probleme
foreach ($contrastIssues as $issue) {
$deduction = match($issue['severity']) {
'high' => 15,
'medium' => 10,
default => 5
};
$baseScore -= $deduction;
}
// Abzug für reine Farbindikatoren
$baseScore -= count($colorOnlyIndicators) * 5;
return max(0, $baseScore);
}
/**
* Generiert Empfehlungen
*/
private function generateRecommendations(array $colors, array $palette, array $duplicates, array $colorsByFormat): array
{
$recommendations = [];
// Duplikate-Empfehlungen
if (count($duplicates) > 0) {
$totalDuplicates = array_sum(array_column($duplicates, 'potential_savings'));
$recommendations[] = "Remove {$totalDuplicates} duplicate colors to reduce CSS complexity and improve maintainability.";
}
// Format-Empfehlungen
$formatCounts = array_map('count', $colorsByFormat);
if (count($formatCounts) > 3) {
$recommendations[] = 'Multiple color formats detected. Consider standardizing on fewer formats (e.g., hex for static colors, custom properties for theme colors).';
}
// OKLCH-Empfehlung
if (! isset($colorsByFormat['oklch']) && count($colors) > 10) {
$recommendations[] = 'Consider using OKLCH format for more perceptually uniform color interpolation and better color consistency.';
}
// Palette-Empfehlungen
$totalColors = count($colors);
if ($totalColors > 50) {
$recommendations[] = "Large color palette detected ({$totalColors} colors). Consider consolidating similar colors into a smaller, more manageable palette.";
}
if (count($palette['primary_colors']) === 0 && $totalColors > 5) {
$recommendations[] = 'No primary brand colors identified. Consider establishing clear primary colors for brand consistency.';
}
if (count($palette['neutral_colors']) < 3 && $totalColors > 10) {
$recommendations[] = 'Limited neutral color palette. Consider adding more gray tones for better design flexibility.';
}
// Semantic Color Empfehlungen
if (count($palette['semantic_colors']) === 0 && $totalColors > 8) {
$recommendations[] = 'No semantic colors found. Consider adding success, error, warning, and info colors for UI feedback.';
}
if (empty($recommendations)) {
$recommendations[] = 'Color system is well organized. Consider documenting color usage guidelines and accessibility standards.';
}
return $recommendations;
}
}

View File

@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Analyzer;
use App\Framework\Design\ValueObjects\ComponentPattern;
/**
* Ergebnis der Component-Pattern-Erkennung
*/
final readonly class ComponentDetectionResult
{
/**
* @param ComponentPattern[] $bemComponents
* @param ComponentPattern[] $utilityComponents
* @param ComponentPattern[] $traditionalComponents
*/
public function __construct(
public int $totalComponents,
public array $bemComponents,
public array $utilityComponents,
public array $traditionalComponents,
public array $patternStatistics,
public array $recommendations
) {
}
/**
* Gibt die dominante Pattern-Methodik zurück
*/
public function getDominantPattern(): string
{
$counts = [
'bem' => count($this->bemComponents),
'utility' => count($this->utilityComponents),
'traditional' => count($this->traditionalComponents),
];
$max = max($counts);
if ($max === 0) {
return 'none';
}
return array_search($max, $counts);
}
/**
* Gibt Pattern-Diversity-Score zurück (0-100)
*/
public function getPatternDiversity(): int
{
$patterns = [$this->bemComponents, $this->utilityComponents, $this->traditionalComponents];
$nonEmptyPatterns = array_filter($patterns, fn ($p) => ! empty($p));
if (empty($nonEmptyPatterns)) {
return 0;
}
// Je mehr verschiedene Pattern-Typen verwendet werden, desto höher die Diversity
$diversityScore = (count($nonEmptyPatterns) / 3) * 100;
return (int) round($diversityScore);
}
/**
* Gibt Konsistenz-Score zurück (0-100)
*/
public function getConsistencyScore(): int
{
$totalComponents = $this->totalComponents;
if ($totalComponents === 0) {
return 100;
}
$dominantCount = max(
count($this->bemComponents),
count($this->utilityComponents),
count($this->traditionalComponents)
);
// Je höher der Anteil der dominanten Methodik, desto konsistenter
$consistencyScore = ($dominantCount / $totalComponents) * 100;
return (int) round($consistencyScore);
}
/**
* Gibt empfohlene Aktionen basierend auf der Analyse zurück
*/
public function getActionableRecommendations(): array
{
$actions = [];
$consistency = $this->getConsistencyScore();
$diversity = $this->getPatternDiversity();
if ($consistency < 60 && $diversity > 80) {
$actions[] = [
'priority' => 'high',
'action' => 'standardize_methodology',
'description' => 'Standardize on one primary CSS methodology to improve consistency.',
];
}
if (count($this->bemComponents) > 5 && count($this->utilityComponents) < 3) {
$actions[] = [
'priority' => 'medium',
'action' => 'add_utilities',
'description' => 'Add utility classes for common spacing, colors, and typography.',
];
}
if (count($this->traditionalComponents) > 10) {
$actions[] = [
'priority' => 'medium',
'action' => 'refactor_to_bem',
'description' => 'Consider refactoring traditional components to BEM methodology.',
];
}
return $actions;
}
/**
* Gibt Pattern-Verteilung zurück
*/
public function getPatternDistribution(): array
{
$total = $this->totalComponents;
if ($total === 0) {
return [
'bem' => 0,
'utility' => 0,
'traditional' => 0,
];
}
return [
'bem' => round((count($this->bemComponents) / $total) * 100, 1),
'utility' => round((count($this->utilityComponents) / $total) * 100, 1),
'traditional' => round((count($this->traditionalComponents) / $total) * 100, 1),
];
}
/**
* Konvertiert zu Array für Export
*/
public function toArray(): array
{
return [
'total_components' => $this->totalComponents,
'bem_components' => array_map(fn (ComponentPattern $p) => $p->toArray(), $this->bemComponents),
'utility_components' => array_map(fn (ComponentPattern $p) => $p->toArray(), $this->utilityComponents),
'traditional_components' => array_map(fn (ComponentPattern $p) => $p->toArray(), $this->traditionalComponents),
'pattern_statistics' => $this->patternStatistics,
'recommendations' => $this->recommendations,
'dominant_pattern' => $this->getDominantPattern(),
'pattern_diversity' => $this->getPatternDiversity(),
'consistency_score' => $this->getConsistencyScore(),
'actionable_recommendations' => $this->getActionableRecommendations(),
];
}
}

View File

@@ -0,0 +1,397 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Analyzer;
use App\Framework\Design\Parser\ClassNameParser;
use App\Framework\Design\ValueObjects\ComponentPatternType;
use App\Framework\Design\ValueObjects\CssParseResult;
/**
* Erkennt Component-Patterns in CSS
*/
final readonly class ComponentDetector
{
public function __construct(
private ClassNameParser $classNameParser = new ClassNameParser()
) {
}
/**
* Erkennt alle Components im CSS
*/
public function detectComponents(CssParseResult $parseResult): ComponentDetectionResult
{
$classNames = array_values($parseResult->classNames);
if (empty($classNames)) {
return new ComponentDetectionResult(
totalComponents: 0,
bemComponents: [],
utilityComponents: [],
traditionalComponents: [],
patternStatistics: [],
recommendations: ['No CSS classes found to analyze for component patterns.']
);
}
// Pattern-Erkennung
$patterns = $this->classNameParser->detectPatterns($classNames);
// Gruppierung nach Pattern-Typ
$bemComponents = [];
$utilityComponents = [];
$traditionalComponents = [];
foreach ($patterns as $pattern) {
switch ($pattern->type) {
case ComponentPatternType::BEM:
$bemComponents[] = $pattern;
break;
case ComponentPatternType::UTILITY:
$utilityComponents[] = $pattern;
break;
case ComponentPatternType::COMPONENT:
$traditionalComponents[] = $pattern;
break;
}
}
// Statistiken erstellen
$statistics = $this->generatePatternStatistics($patterns, $classNames);
// Empfehlungen generieren
$recommendations = $this->generateRecommendations($bemComponents, $utilityComponents, $traditionalComponents, $classNames);
return new ComponentDetectionResult(
totalComponents: count($patterns),
bemComponents: $bemComponents,
utilityComponents: $utilityComponents,
traditionalComponents: $traditionalComponents,
patternStatistics: $statistics,
recommendations: $recommendations
);
}
/**
* Analysiert spezifisch BEM-Patterns
*/
public function analyzeBemPatterns(CssParseResult $parseResult): array
{
$bemClasses = $parseResult->getBemClasses();
$analysis = [];
foreach ($bemClasses as $className) {
$block = $className->getBemBlock();
if (! isset($analysis[$block])) {
$analysis[$block] = [
'block' => $block,
'elements' => [],
'modifiers' => [],
'total_classes' => 0,
'completeness' => 'unknown',
];
}
$analysis[$block]['total_classes']++;
if ($className->isBemElement()) {
$element = $className->getBemElement();
if ($element && ! in_array($element, $analysis[$block]['elements'])) {
$analysis[$block]['elements'][] = $element;
}
}
if ($className->isBemModifier()) {
$modifier = $className->getBemModifier();
if ($modifier && ! in_array($modifier, $analysis[$block]['modifiers'])) {
$analysis[$block]['modifiers'][] = $modifier;
}
}
}
// Completeness-Bewertung
foreach ($analysis as $block => &$data) {
$hasElements = ! empty($data['elements']);
$hasModifiers = ! empty($data['modifiers']);
if (! $hasElements && ! $hasModifiers) {
$data['completeness'] = 'block_only';
} elseif ($hasElements && ! $hasModifiers) {
$data['completeness'] = 'block_with_elements';
} elseif (! $hasElements && $hasModifiers) {
$data['completeness'] = 'block_with_modifiers';
} else {
$data['completeness'] = 'full_bem';
}
}
return $analysis;
}
/**
* Analysiert Utility-Class-Patterns
*/
public function analyzeUtilityPatterns(CssParseResult $parseResult): array
{
$utilityClasses = $parseResult->getUtilityClasses();
$analysis = [];
foreach ($utilityClasses as $className) {
$category = $className->getUtilityCategory();
if (! $category) {
continue;
}
if (! isset($analysis[$category])) {
$analysis[$category] = [
'category' => $category,
'classes' => [],
'count' => 0,
'coverage' => 'unknown',
];
}
$analysis[$category]['classes'][] = $className->name;
$analysis[$category]['count']++;
}
// Coverage-Bewertung für verschiedene Kategorien
foreach ($analysis as $category => &$data) {
$data['coverage'] = $this->assessUtilityCoverage($category, $data['classes']);
}
return $analysis;
}
/**
* Findet potenzielle Component-Candidates
*/
public function findComponentCandidates(CssParseResult $parseResult): array
{
$candidates = [];
$selectorFrequency = [];
// Analysiere Selektor-Häufigkeiten
foreach ($parseResult->rules as $rule) {
foreach ($rule->selectors as $selector) {
if ($selector->getType()->value === 'class') {
$classes = $selector->extractClasses();
foreach ($classes as $class) {
if (! isset($selectorFrequency[$class])) {
$selectorFrequency[$class] = 0;
}
$selectorFrequency[$class]++;
}
}
}
}
// Finde Klassen mit hoher Verwendung (potenzielle Components)
foreach ($selectorFrequency as $className => $frequency) {
if ($frequency >= 3) { // Threshold für Component-Candidate
$candidates[] = [
'class' => $className,
'frequency' => $frequency,
'potential' => $this->assessComponentPotential($className, $frequency),
'suggestions' => $this->getComponentSuggestions($className, $frequency),
];
}
}
// Sortiere nach Häufigkeit
usort($candidates, fn ($a, $b) => $b['frequency'] <=> $a['frequency']);
return $candidates;
}
/**
* Generiert Pattern-Statistiken
*/
private function generatePatternStatistics(array $patterns, array $classNames): array
{
$totalClasses = count($classNames);
$statistics = [
'total_patterns' => count($patterns),
'total_classes' => $totalClasses,
'pattern_distribution' => [],
'complexity_analysis' => [],
];
$patternTypes = [];
foreach ($patterns as $pattern) {
$type = $pattern->type->value;
if (! isset($patternTypes[$type])) {
$patternTypes[$type] = 0;
}
$patternTypes[$type]++;
}
foreach ($patternTypes as $type => $count) {
$percentage = $totalClasses > 0 ? round(($count / $totalClasses) * 100, 1) : 0;
$statistics['pattern_distribution'][$type] = [
'count' => $count,
'percentage' => $percentage,
];
}
// Complexity Analysis
foreach ($patterns as $pattern) {
$complexity = $pattern->getComplexity();
if (! isset($statistics['complexity_analysis'][$complexity])) {
$statistics['complexity_analysis'][$complexity] = 0;
}
$statistics['complexity_analysis'][$complexity]++;
}
return $statistics;
}
/**
* Generiert Empfehlungen
*/
private function generateRecommendations(array $bemComponents, array $utilityComponents, array $traditionalComponents, array $classNames): array
{
$recommendations = [];
$totalClasses = count($classNames);
// BEM-Empfehlungen
if (empty($bemComponents) && $totalClasses > 10) {
$recommendations[] = 'Consider adopting BEM methodology for better component organization and naming consistency.';
} elseif (! empty($bemComponents)) {
$blocksWithoutElements = 0;
foreach ($bemComponents as $component) {
if (empty($component->metadata['elements'])) {
$blocksWithoutElements++;
}
}
if ($blocksWithoutElements > count($bemComponents) / 2) {
$recommendations[] = 'Many BEM blocks lack elements. Consider breaking down components into smaller, reusable parts.';
}
}
// Utility-Empfehlungen
if (! empty($utilityComponents)) {
$utilityCount = array_sum(array_map(fn ($comp) => $comp->metadata['class_count'], $utilityComponents));
$utilityPercentage = round(($utilityCount / $totalClasses) * 100, 1);
if ($utilityPercentage > 70) {
$recommendations[] = 'High percentage of utility classes detected. Consider consolidating repetitive patterns into components.';
} elseif ($utilityPercentage < 10 && $totalClasses > 20) {
$recommendations[] = 'Low usage of utility classes. Consider adding utility classes for common spacing, colors, and typography.';
}
}
// Mixed Pattern Empfehlungen
$hasMultiplePatterns = (! empty($bemComponents) ? 1 : 0) +
(! empty($utilityComponents) ? 1 : 0) +
(! empty($traditionalComponents) ? 1 : 0);
if ($hasMultiplePatterns >= 3) {
$recommendations[] = 'Multiple CSS methodologies detected. Consider standardizing on one primary approach for consistency.';
}
// Spezifische Pattern-Empfehlungen
if (! empty($traditionalComponents)) {
$componentTypes = array_unique(array_map(fn ($comp) => $comp->metadata['component_type'], $traditionalComponents));
if (in_array('button', $componentTypes) && in_array('form', $componentTypes)) {
$recommendations[] = 'Consider creating a design system with consistent button and form component patterns.';
}
}
if (empty($recommendations)) {
$recommendations[] = 'Component patterns look well organized. Consider documenting the design system for team consistency.';
}
return $recommendations;
}
/**
* Bewertet Utility-Coverage
*/
private function assessUtilityCoverage(string $category, array $classes): string
{
$expectedCoverage = [
'spacing' => ['xs', 'sm', 'md', 'lg', 'xl'],
'color' => ['primary', 'secondary', 'success', 'warning', 'error'],
'typography' => ['xs', 'sm', 'base', 'lg', 'xl'],
'sizing' => ['sm', 'md', 'lg', 'full'],
];
if (! isset($expectedCoverage[$category])) {
return 'unknown';
}
$expected = $expectedCoverage[$category];
$hasExpected = 0;
foreach ($expected as $expectedClass) {
foreach ($classes as $actualClass) {
if (str_contains($actualClass, $expectedClass)) {
$hasExpected++;
break;
}
}
}
$percentage = count($expected) > 0 ? ($hasExpected / count($expected)) : 0;
if ($percentage >= 0.8) {
return 'excellent';
} elseif ($percentage >= 0.6) {
return 'good';
} elseif ($percentage >= 0.4) {
return 'fair';
} else {
return 'poor';
}
}
/**
* Bewertet Component-Potenzial
*/
private function assessComponentPotential(string $className, int $frequency): string
{
if ($frequency >= 10) {
return 'high';
} elseif ($frequency >= 6) {
return 'medium';
} else {
return 'low';
}
}
/**
* Gibt Component-Verbesserungsvorschläge
*/
private function getComponentSuggestions(string $className, int $frequency): array
{
$suggestions = [];
if ($frequency >= 8) {
$suggestions[] = 'Consider extracting into a reusable component or design token.';
}
if (str_contains($className, 'btn') || str_contains($className, 'button')) {
$suggestions[] = 'Consider creating button variants (primary, secondary, outline).';
}
if (str_contains($className, 'card')) {
$suggestions[] = 'Consider standardizing card padding, borders, and shadows.';
}
return $suggestions;
}
}

View File

@@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Analyzer;
/**
* Ergebnis der Convention-Prüfung
*/
final readonly class ConventionCheckResult
{
public function __construct(
public int $overallScore,
public array $categoryScores,
public array $violations,
public array $recommendations,
public string $conformanceLevel
) {
}
/**
* Gruppiert Violations nach Schweregrad
*/
public function getViolationsBySeverity(): array
{
$grouped = [
'high' => [],
'medium' => [],
'low' => [],
];
foreach ($this->violations as $violation) {
$severity = $violation['severity'] ?? 'low';
$grouped[$severity][] = $violation;
}
return $grouped;
}
/**
* Gibt die schlimmsten Problembereiche zurück
*/
public function getWorstAreas(): array
{
$sortedScores = $this->categoryScores;
asort($sortedScores);
return array_slice($sortedScores, 0, 3, true);
}
/**
* Gibt die besten Bereiche zurück
*/
public function getBestAreas(): array
{
$sortedScores = $this->categoryScores;
arsort($sortedScores);
return array_slice($sortedScores, 0, 3, true);
}
/**
* Gibt priorisierte Action Items zurück
*/
public function getPrioritizedActions(): array
{
$actions = [];
$violationsBySeverity = $this->getViolationsBySeverity();
// High-Priority Violations zuerst
foreach ($violationsBySeverity['high'] as $violation) {
$actions[] = [
'priority' => 'high',
'category' => $violation['type'],
'action' => $violation['suggestion'],
'impact' => 'accessibility_maintainability',
];
}
// Medium-Priority basierend auf schlechtesten Scores
$worstAreas = $this->getWorstAreas();
foreach ($worstAreas as $area => $score) {
if ($score < 70) {
$actions[] = [
'priority' => 'medium',
'category' => $area,
'action' => $this->getAreaImprovementAction($area),
'impact' => 'code_quality',
];
}
}
return $actions;
}
/**
* Berechnet Verbesserungs-Potenzial
*/
public function getImprovementPotential(): array
{
$potential = [];
foreach ($this->categoryScores as $category => $score) {
$maxImprovement = 100 - $score;
$effort = $this->getEstimatedEffort($category, $score);
$potential[$category] = [
'current_score' => $score,
'max_improvement' => $maxImprovement,
'estimated_effort' => $effort,
'roi_score' => $maxImprovement / max(1, $effort), // Return on Investment
];
}
// Sortiere nach ROI
uasort($potential, fn ($a, $b) => $b['roi_score'] <=> $a['roi_score']);
return $potential;
}
/**
* Konvertiert zu Array für Export
*/
public function toArray(): array
{
return [
'overall_score' => $this->overallScore,
'conformance_level' => $this->conformanceLevel,
'category_scores' => $this->categoryScores,
'violations' => $this->violations,
'violations_by_severity' => $this->getViolationsBySeverity(),
'recommendations' => $this->recommendations,
'worst_areas' => $this->getWorstAreas(),
'best_areas' => $this->getBestAreas(),
'prioritized_actions' => $this->getPrioritizedActions(),
'improvement_potential' => $this->getImprovementPotential(),
];
}
/**
* Gibt bereichsspezifische Verbesserungs-Actions
*/
private function getAreaImprovementAction(string $area): string
{
return match($area) {
'naming' => 'Standardize naming conventions across all CSS classes',
'specificity' => 'Refactor high-specificity selectors and reduce !important usage',
'organization' => 'Reorganize CSS properties within rules for better readability',
'custom_properties' => 'Review and improve custom property naming and organization',
'accessibility' => 'Add missing focus styles and improve keyboard navigation support',
default => "Improve {$area} conventions and standards"
};
}
/**
* Schätzt Aufwand für Verbesserungen
*/
private function getEstimatedEffort(string $category, int|float $score): int
{
$baseEffort = match($category) {
'naming' => 3, // Moderate effort - mostly renaming
'specificity' => 4, // Higher effort - requires structural changes
'organization' => 2, // Lower effort - mostly reordering
'custom_properties' => 2, // Lower effort - renaming and reorganization
'accessibility' => 3, // Moderate effort - adding new styles
default => 3
};
// Adjust based on how bad the score is
$severityMultiplier = match(true) {
$score >= 80 => 0.5,
$score >= 60 => 1.0,
$score >= 40 => 1.5,
default => 2.0
};
return (int) ceil($baseEffort * $severityMultiplier);
}
}

View File

@@ -0,0 +1,442 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Analyzer;
use App\Framework\Design\ValueObjects\CssParseResult;
/**
* Prüft CSS-Naming-Conventions und Code-Standards
*/
final readonly class ConventionChecker
{
/**
* Prüft alle Conventions im CSS
*/
public function checkConventions(CssParseResult $parseResult): ConventionCheckResult
{
$violations = [];
$scores = [];
// Naming Convention Check
$namingResult = $this->checkNamingConventions($parseResult);
$scores['naming'] = $namingResult['score'];
if (! empty($namingResult['violations'])) {
$violations = array_merge($violations, $namingResult['violations']);
}
// Selector Specificity Check
$specificityResult = $this->checkSelectorSpecificity($parseResult);
$scores['specificity'] = $specificityResult['score'];
if (! empty($specificityResult['violations'])) {
$violations = array_merge($violations, $specificityResult['violations']);
}
// Property Organization Check
$organizationResult = $this->checkPropertyOrganization($parseResult);
$scores['organization'] = $organizationResult['score'];
if (! empty($organizationResult['violations'])) {
$violations = array_merge($violations, $organizationResult['violations']);
}
// Custom Property Convention Check
$customPropResult = $this->checkCustomPropertyConventions($parseResult);
$scores['custom_properties'] = $customPropResult['score'];
if (! empty($customPropResult['violations'])) {
$violations = array_merge($violations, $customPropResult['violations']);
}
// Accessibility Check
$accessibilityResult = $this->checkAccessibilityPatterns($parseResult);
$scores['accessibility'] = $accessibilityResult['score'];
if (! empty($accessibilityResult['violations'])) {
$violations = array_merge($violations, $accessibilityResult['violations']);
}
// Berechne Gesamt-Score
$overallScore = array_sum($scores) / count($scores);
// Generiere Empfehlungen
$recommendations = $this->generateRecommendations($scores, $violations);
return new ConventionCheckResult(
overallScore: (int) round($overallScore),
categoryScores: $scores,
violations: $violations,
recommendations: $recommendations,
conformanceLevel: $this->getConformanceLevel($overallScore)
);
}
/**
* Prüft Naming Conventions
*/
private function checkNamingConventions(CssParseResult $parseResult): array
{
$violations = [];
$analysis = $parseResult->getNamingConventionAnalysis();
$totalClasses = $analysis['total_classes'];
$dominantConvention = $analysis['dominant_convention'];
if ($totalClasses === 0) {
return ['score' => 100, 'violations' => []];
}
// Prüfe Konsistenz
$dominantCount = $analysis[$dominantConvention] ?? 0;
$consistencyPercentage = ($dominantCount / $totalClasses) * 100;
if ($consistencyPercentage < 70) {
$violations[] = [
'type' => 'naming_inconsistency',
'severity' => 'medium',
'message' => "Mixed naming conventions detected. {$dominantConvention} is dominant with {$consistencyPercentage}% usage.",
'suggestion' => 'Standardize on one naming convention across your CSS.',
];
}
// Prüfe spezifische Convention-Violations
foreach ($analysis['violations'] as $violation) {
$violations[] = [
'type' => 'naming_violation',
'severity' => 'low',
'message' => "Class '{$violation}' doesn't follow standard naming conventions.",
'suggestion' => 'Use consistent casing and separators (kebab-case, camelCase, or BEM).',
];
}
// BEM-spezifische Checks
if ($dominantConvention === 'bem') {
$bemViolations = $this->checkBemConventions($parseResult);
$violations = array_merge($violations, $bemViolations);
}
$score = max(0, 100 - (count($violations) * 10));
return ['score' => $score, 'violations' => $violations];
}
/**
* Prüft Selector-Spezifität
*/
private function checkSelectorSpecificity(CssParseResult $parseResult): array
{
$violations = [];
$highSpecificityCount = 0;
$totalSelectors = 0;
foreach ($parseResult->rules as $rule) {
foreach ($rule->selectors as $selector) {
$totalSelectors++;
$specificity = $selector->calculateSpecificity();
// Sehr hohe Spezifität (>100)
if ($specificity > 100) {
$highSpecificityCount++;
$violations[] = [
'type' => 'high_specificity',
'severity' => 'medium',
'message' => "Selector '{$selector->value}' has high specificity ({$specificity}).",
'suggestion' => 'Reduce specificity by avoiding IDs and deeply nested selectors.',
];
}
// Sehr niedrige Spezifität mit !important
foreach ($rule->properties as $property) {
if ($property->isImportant() && $specificity < 10) {
$violations[] = [
'type' => 'important_overuse',
'severity' => 'high',
'message' => "Property '{$property->name}' uses !important with low specificity selector.",
'suggestion' => 'Increase selector specificity instead of using !important.',
];
}
}
}
}
$highSpecificityPercentage = $totalSelectors > 0 ? ($highSpecificityCount / $totalSelectors) * 100 : 0;
$score = max(0, 100 - ($highSpecificityPercentage * 2));
return ['score' => (int) $score, 'violations' => $violations];
}
/**
* Prüft Property-Organisation
*/
private function checkPropertyOrganization(CssParseResult $parseResult): array
{
$violations = [];
$propertyOrderViolations = 0;
$totalRules = count($parseResult->rules);
// Erwartete Property-Reihenfolge (grob)
$expectedOrder = [
'position', 'top', 'right', 'bottom', 'left', 'z-index',
'display', 'flex', 'grid', 'float', 'clear',
'width', 'height', 'margin', 'padding',
'border', 'background', 'color',
'font', 'text', 'line-height',
'transform', 'transition', 'animation',
];
foreach ($parseResult->rules as $rule) {
if (count($rule->properties) < 3) {
continue; // Skip kleine Rules
}
$propertyOrder = array_map(fn ($prop) => $prop->name, $rule->properties);
// Vereinfachte Reihenfolgen-Prüfung
$lastOrderIndex = -1;
$orderViolation = false;
foreach ($propertyOrder as $property) {
$orderIndex = $this->findPropertyOrderIndex($property, $expectedOrder);
if ($orderIndex !== -1 && $orderIndex < $lastOrderIndex) {
$orderViolation = true;
break;
}
if ($orderIndex !== -1) {
$lastOrderIndex = $orderIndex;
}
}
if ($orderViolation) {
$propertyOrderViolations++;
}
}
if ($propertyOrderViolations > 0 && $totalRules > 0) {
$violationPercentage = ($propertyOrderViolations / $totalRules) * 100;
if ($violationPercentage > 30) {
$violations[] = [
'type' => 'property_order',
'severity' => 'low',
'message' => "{$violationPercentage}% of rules have suboptimal property ordering.",
'suggestion' => 'Consider organizing properties by type: positioning, box model, visual, typography.',
];
}
}
$score = max(0, 100 - ($propertyOrderViolations * 5));
return ['score' => $score, 'violations' => $violations];
}
/**
* Prüft Custom Property Conventions
*/
private function checkCustomPropertyConventions(CssParseResult $parseResult): array
{
$violations = [];
$customProperties = $parseResult->customProperties;
if (empty($customProperties)) {
return ['score' => 80, 'violations' => []]; // Neutral score when no custom properties
}
foreach ($customProperties as $token) {
$name = $token->name;
// Prüfe Naming Convention
if (! preg_match('/^[a-z][a-z0-9-]*$/', $name)) {
$violations[] = [
'type' => 'custom_property_naming',
'severity' => 'medium',
'message' => "Custom property '--{$name}' doesn't follow kebab-case convention.",
'suggestion' => 'Use lowercase letters and hyphens for custom property names.',
];
}
// Prüfe semantische Namen
if (preg_match('/^(red|blue|green|yellow)-\d+$/', $name)) {
$violations[] = [
'type' => 'semantic_naming',
'severity' => 'low',
'message' => "Custom property '--{$name}' uses literal color naming.",
'suggestion' => 'Consider semantic names like --color-primary, --color-error instead.',
];
}
}
$score = max(0, 100 - (count($violations) * 15));
return ['score' => $score, 'violations' => $violations];
}
/**
* Prüft Accessibility Patterns
*/
private function checkAccessibilityPatterns(CssParseResult $parseResult): array
{
$violations = [];
$accessibilityScore = 100;
// Prüfe auf focus-Styles
$hasFocusStyles = false;
$hasColorOnlyIndicators = false;
foreach ($parseResult->rules as $rule) {
foreach ($rule->selectors as $selector) {
if (str_contains($selector->value, ':focus')) {
$hasFocusStyles = true;
}
// Prüfe auf outline: none ohne Alternative
if (str_contains($selector->value, ':focus')) {
foreach ($rule->properties as $property) {
if ($property->name === 'outline' && $property->value === 'none') {
$hasAlternativeFocus = false;
foreach ($rule->properties as $altProp) {
if (in_array($altProp->name, ['border', 'box-shadow', 'background'])) {
$hasAlternativeFocus = true;
break;
}
}
if (! $hasAlternativeFocus) {
$violations[] = [
'type' => 'focus_accessibility',
'severity' => 'high',
'message' => 'Focus outline removed without alternative focus indicator.',
'suggestion' => 'Provide alternative focus indicators when removing default outline.',
];
}
}
}
}
}
}
if (! $hasFocusStyles && count($parseResult->rules) > 5) {
$violations[] = [
'type' => 'missing_focus_styles',
'severity' => 'medium',
'message' => 'No focus styles detected in CSS.',
'suggestion' => 'Add focus styles for interactive elements to improve keyboard accessibility.',
];
$accessibilityScore -= 20;
}
return ['score' => $accessibilityScore, 'violations' => $violations];
}
/**
* Prüft BEM-spezifische Conventions
*/
private function checkBemConventions(CssParseResult $parseResult): array
{
$violations = [];
$bemClasses = $parseResult->getBemClasses();
foreach ($bemClasses as $className) {
$name = $className->name;
// Prüfe BEM Syntax-Korrektheit
if ($className->isBemElement() && substr_count($name, '__') > 1) {
$violations[] = [
'type' => 'bem_syntax',
'severity' => 'medium',
'message' => "Class '{$name}' has multiple element separators (__). BEM allows only one level of elements.",
'suggestion' => 'Use only one level of elements in BEM: block__element--modifier.',
];
}
if ($className->isBemModifier() && substr_count($name, '--') > 1) {
$violations[] = [
'type' => 'bem_syntax',
'severity' => 'medium',
'message' => "Class '{$name}' has multiple modifier separators (--). Use single modifier per class.",
'suggestion' => 'Use single modifiers: block__element--modifier.',
];
}
}
return $violations;
}
/**
* Findet Property-Order-Index
*/
private function findPropertyOrderIndex(string $property, array $order): int
{
foreach ($order as $index => $orderProperty) {
if (str_starts_with($property, $orderProperty)) {
return $index;
}
}
return -1;
}
/**
* Generiert Empfehlungen
*/
private function generateRecommendations(array $scores, array $violations): array
{
$recommendations = [];
// Basiere Empfehlungen auf Scores
foreach ($scores as $category => $score) {
if ($score < 70) {
$recommendations[] = $this->getCategoryRecommendation($category, $score);
}
}
// Prioritäts-basierte Empfehlungen
$highSeverityCount = count(array_filter($violations, fn ($v) => $v['severity'] === 'high'));
$mediumSeverityCount = count(array_filter($violations, fn ($v) => $v['severity'] === 'medium'));
if ($highSeverityCount > 0) {
$recommendations[] = "Address {$highSeverityCount} high-priority violations first for accessibility and maintainability.";
}
if ($mediumSeverityCount > 3) {
$recommendations[] = "Consider refactoring to address {$mediumSeverityCount} medium-priority issues for better code quality.";
}
if (empty($recommendations)) {
$recommendations[] = 'CSS conventions are well maintained. Consider documenting your standards for team consistency.';
}
return $recommendations;
}
/**
* Gibt kategorie-spezifische Empfehlungen
*/
private function getCategoryRecommendation(string $category, int $score): string
{
return match($category) {
'naming' => "Naming conventions need improvement (Score: {$score}/100). Standardize on one naming methodology.",
'specificity' => "Selector specificity issues detected (Score: {$score}/100). Avoid overly specific selectors and !important.",
'organization' => "Property organization could be better (Score: {$score}/100). Group related properties together.",
'custom_properties' => "Custom property conventions need attention (Score: {$score}/100). Use consistent, semantic naming.",
'accessibility' => "Accessibility patterns missing (Score: {$score}/100). Add focus styles and ensure color contrast.",
default => "Category '{$category}' needs improvement (Score: {$score}/100)."
};
}
/**
* Bestimmt Conformance Level
*/
private function getConformanceLevel(float $score): string
{
return match(true) {
$score >= 90 => 'excellent',
$score >= 80 => 'good',
$score >= 70 => 'fair',
$score >= 60 => 'needs_improvement',
default => 'poor'
};
}
}

View File

@@ -0,0 +1,387 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Analyzer;
use App\Framework\Design\ValueObjects\CssParseResult;
use App\Framework\Filesystem\FilePath;
/**
* Vollständige Design System Analyse - Haupt-Ergebnis-Klasse
*/
final readonly class DesignSystemAnalysis
{
/**
* @param FilePath[] $sourceFiles
* @param CssParseResult[] $parseResults
*/
public function __construct(
public array $sourceFiles,
public array $parseResults,
public CssParseResult $combinedResult,
public TokenAnalysisResult $tokenAnalysis,
public ComponentDetectionResult $componentAnalysis,
public ConventionCheckResult $conventionAnalysis,
public ColorAnalysisResult $colorAnalysis,
public array $overallStatistics
) {
}
/**
* Berechnet Gesamt-Design-System-Score (0-100)
*/
public function getOverallDesignSystemScore(): int
{
$scores = [
'tokens' => $this->getTokenScore(),
'components' => $this->getComponentScore(),
'conventions' => $this->conventionAnalysis->overallScore,
'colors' => $this->getColorScore(),
];
// Gewichtung der verschiedenen Bereiche
$weights = [
'tokens' => 0.25,
'components' => 0.25,
'conventions' => 0.30,
'colors' => 0.20,
];
$weightedScore = 0;
foreach ($scores as $area => $score) {
$weightedScore += $score * $weights[$area];
}
return (int) round($weightedScore);
}
/**
* Gibt die Reife-Stufe des Design Systems zurück
*/
public function getMaturityLevel(): string
{
$score = $this->getOverallDesignSystemScore();
$tokenUsage = $this->tokenAnalysis->getTokenCoverage()['usage_percentage'];
$hasComponents = $this->componentAnalysis->totalComponents > 0;
$colorConsistency = $this->colorAnalysis->getConsistencyScore();
return match(true) {
$score >= 90 && $tokenUsage >= 80 && $hasComponents && $colorConsistency >= 80 => 'mature',
$score >= 75 && $tokenUsage >= 60 && $hasComponents => 'established',
$score >= 60 && ($tokenUsage >= 40 || $hasComponents) => 'developing',
$score >= 40 => 'emerging',
default => 'basic'
};
}
/**
* Gibt kritische Problembereiche zurück
*/
public function getCriticalIssues(): array
{
$issues = [];
// Token-Probleme
if ($this->tokenAnalysis->totalTokens === 0) {
$issues[] = [
'severity' => 'high',
'category' => 'tokens',
'issue' => 'No design tokens found',
'impact' => 'Inconsistent styling and difficult maintenance',
'recommendation' => 'Implement CSS custom properties for colors, spacing, and typography',
];
} elseif (count($this->tokenAnalysis->unusedTokens) > $this->tokenAnalysis->totalTokens * 0.5) {
$issues[] = [
'severity' => 'medium',
'category' => 'tokens',
'issue' => 'Many unused design tokens',
'impact' => 'Bloated CSS and confusing token system',
'recommendation' => 'Remove unused tokens and audit token usage',
];
}
// Component-Probleme
if ($this->componentAnalysis->getConsistencyScore() < 50) {
$issues[] = [
'severity' => 'high',
'category' => 'components',
'issue' => 'Inconsistent component patterns',
'impact' => 'Poor maintainability and team confusion',
'recommendation' => 'Standardize on one CSS methodology (BEM, Utility-first, etc.)',
];
}
// Convention-Probleme
$highSeverityViolations = $this->conventionAnalysis->getViolationsBySeverity()['high'];
if (! empty($highSeverityViolations)) {
$issues[] = [
'severity' => 'high',
'category' => 'conventions',
'issue' => count($highSeverityViolations) . ' high-severity convention violations',
'impact' => 'Accessibility and usability problems',
'recommendation' => 'Address accessibility issues, especially focus styles and contrast',
];
}
// Farb-Probleme
$worstContrastPairs = $this->colorAnalysis->getWorstContrastPairs();
if (! empty($worstContrastPairs)) {
$issues[] = [
'severity' => 'high',
'category' => 'colors',
'issue' => count($worstContrastPairs) . ' color combinations fail WCAG contrast requirements',
'impact' => 'Poor accessibility for users with visual impairments',
'recommendation' => 'Improve color contrast ratios to meet WCAG AA standards',
];
}
return $issues;
}
/**
* Gibt Quick-Win-Verbesserungen zurück
*/
public function getQuickWins(): array
{
$quickWins = [];
// Duplikate entfernen
if (! empty($this->colorAnalysis->duplicateColors)) {
$totalDuplicates = array_sum(array_column($this->colorAnalysis->duplicateColors, 'potential_savings'));
$quickWins[] = [
'effort' => 'low',
'impact' => 'medium',
'action' => "Remove {$totalDuplicates} duplicate colors",
'benefit' => 'Smaller CSS size and cleaner color palette',
'estimated_time' => '1-2 hours',
];
}
// Unbenutzte Tokens
if (count($this->tokenAnalysis->unusedTokens) > 0) {
$unusedCount = count($this->tokenAnalysis->unusedTokens);
$quickWins[] = [
'effort' => 'low',
'impact' => 'low',
'action' => "Remove {$unusedCount} unused design tokens",
'benefit' => 'Cleaner token system and reduced CSS complexity',
'estimated_time' => '30 minutes',
];
}
// Naming-Konventionen
if ($this->conventionAnalysis->categoryScores['naming'] < 70) {
$quickWins[] = [
'effort' => 'medium',
'impact' => 'high',
'action' => 'Standardize CSS class naming conventions',
'benefit' => 'Better maintainability and team consistency',
'estimated_time' => '2-4 hours',
];
}
// Fehlende Standard-Tokens
if (! empty($this->tokenAnalysis->missingTokens)) {
$quickWins[] = [
'effort' => 'medium',
'impact' => 'high',
'action' => 'Add missing standard design tokens',
'benefit' => 'More complete design system and better consistency',
'estimated_time' => '1-3 hours',
];
}
return $quickWins;
}
/**
* Gibt Entwicklungsroadmap zurück
*/
public function getDevelopmentRoadmap(): array
{
$maturity = $this->getMaturityLevel();
$roadmap = match($maturity) {
'basic' => [
'phase_1' => [
'title' => 'Foundation Setup',
'duration' => '1-2 weeks',
'tasks' => [
'Establish CSS custom properties for core colors',
'Define basic spacing scale',
'Standardize naming conventions',
'Create utility classes for common patterns',
],
],
'phase_2' => [
'title' => 'Component Library',
'duration' => '2-3 weeks',
'tasks' => [
'Build core component patterns',
'Document component usage',
'Implement accessibility standards',
'Create component naming guidelines',
],
],
],
'emerging' => [
'phase_1' => [
'title' => 'Standardization',
'duration' => '1 week',
'tasks' => [
'Fix critical accessibility issues',
'Consolidate duplicate colors',
'Improve token usage consistency',
'Document existing patterns',
],
],
'phase_2' => [
'title' => 'Enhancement',
'duration' => '2 weeks',
'tasks' => [
'Expand design token system',
'Improve component consistency',
'Add semantic color tokens',
'Create design guidelines',
],
],
],
'developing' => [
'phase_1' => [
'title' => 'Optimization',
'duration' => '3-5 days',
'tasks' => [
'Remove unused tokens and styles',
'Improve color contrast compliance',
'Optimize component patterns',
'Enhance documentation',
],
],
],
'established' => [
'phase_1' => [
'title' => 'Advanced Features',
'duration' => '1 week',
'tasks' => [
'Implement advanced color formats (OKLCH)',
'Create automated design token generation',
'Add theme switching capabilities',
'Implement design system automation',
],
],
],
'mature' => [
'phase_1' => [
'title' => 'Maintenance & Innovation',
'duration' => 'Ongoing',
'tasks' => [
'Monitor design system metrics',
'Implement advanced automation',
'Research emerging CSS features',
'Mentor other teams',
],
],
]
};
return [
'current_maturity' => $maturity,
'next_level' => $this->getNextMaturityLevel($maturity),
'phases' => $roadmap,
];
}
/**
* Exportiert vollständigen Report
*/
public function exportReport(): array
{
return [
'meta' => [
'generated_at' => date('c'),
'analyzed_files' => count($this->sourceFiles),
'total_css_size' => $this->overallStatistics['total_content_size'],
'analysis_version' => '1.0',
],
'summary' => [
'overall_score' => $this->getOverallDesignSystemScore(),
'maturity_level' => $this->getMaturityLevel(),
'critical_issues_count' => count($this->getCriticalIssues()),
'quick_wins_count' => count($this->getQuickWins()),
],
'detailed_analysis' => [
'token_analysis' => $this->tokenAnalysis->toArray(),
'component_analysis' => $this->componentAnalysis->toArray(),
'convention_analysis' => $this->conventionAnalysis->toArray(),
'color_analysis' => $this->colorAnalysis->toArray(),
],
'recommendations' => [
'critical_issues' => $this->getCriticalIssues(),
'quick_wins' => $this->getQuickWins(),
'development_roadmap' => $this->getDevelopmentRoadmap(),
],
'statistics' => $this->overallStatistics,
];
}
/**
* Berechnet Token-Score
*/
private function getTokenScore(): int
{
if ($this->tokenAnalysis->totalTokens === 0) {
return 0;
}
$coverage = $this->tokenAnalysis->getTokenCoverage()['usage_percentage'];
$missingTokenPenalty = count($this->tokenAnalysis->missingTokens) * 5;
return max(0, (int) ($coverage - $missingTokenPenalty));
}
/**
* Berechnet Component-Score
*/
private function getComponentScore(): int
{
if ($this->componentAnalysis->totalComponents === 0) {
return 20; // Basis-Score für keine Components
}
$consistency = $this->componentAnalysis->getConsistencyScore();
$diversity = 100 - $this->componentAnalysis->getPatternDiversity(); // Weniger Diversity ist besser für Konsistenz
return (int) (($consistency * 0.7) + ($diversity * 0.3));
}
/**
* Berechnet Color-Score
*/
private function getColorScore(): int
{
if ($this->colorAnalysis->totalColors === 0) {
return 30; // Basis-Score für keine Farben
}
$consistency = $this->colorAnalysis->getConsistencyScore();
$contrastCompliance = $this->colorAnalysis->getContrastComplianceScore();
return (int) (($consistency * 0.4) + ($contrastCompliance * 0.6));
}
/**
* Bestimmt nächste Reife-Stufe
*/
private function getNextMaturityLevel(string $current): string
{
return match($current) {
'basic' => 'emerging',
'emerging' => 'developing',
'developing' => 'established',
'established' => 'mature',
'mature' => 'mature', // Already at top level
default => 'emerging'
};
}
}

View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Analyzer;
use App\Framework\Design\Parser\CssParser;
use App\Framework\Design\ValueObjects\CssParseResult;
use App\Framework\Filesystem\FilePath;
/**
* Haupt-Analyzer für das Design System
*/
final readonly class DesignSystemAnalyzer
{
public function __construct(
private CssParser $cssParser = new CssParser(),
private TokenAnalyzer $tokenAnalyzer = new TokenAnalyzer(),
private ComponentDetector $componentDetector = new ComponentDetector(),
private ConventionChecker $conventionChecker = new ConventionChecker(),
private ColorAnalyzer $colorAnalyzer = new ColorAnalyzer()
) {
}
/**
* Analysiert ein komplettes Design System
*/
public function analyzeDesignSystem(string|array $cssFiles): DesignSystemAnalysis
{
$files = is_array($cssFiles) ? $cssFiles : [$cssFiles];
$parseResults = [];
// Parse alle CSS-Dateien
foreach ($files as $file) {
$filePath = $file instanceof FilePath ? $file : FilePath::create($file);
$parseResults[] = $this->cssParser->parseFile($filePath);
}
// Kombiniere alle Ergebnisse
$combinedResult = $this->combineParseResults($parseResults);
// Durchführung der verschiedenen Analysen
$tokenAnalysis = $this->tokenAnalyzer->analyze($combinedResult);
$componentAnalysis = $this->componentDetector->detectComponents($combinedResult);
$conventionAnalysis = $this->conventionChecker->checkConventions($combinedResult);
$colorAnalysis = $this->colorAnalyzer->analyzeColors($combinedResult);
return new DesignSystemAnalysis(
sourceFiles: array_map(fn ($result) => $result->sourceFile, $parseResults),
parseResults: $parseResults,
combinedResult: $combinedResult,
tokenAnalysis: $tokenAnalysis,
componentAnalysis: $componentAnalysis,
conventionAnalysis: $conventionAnalysis,
colorAnalysis: $colorAnalysis,
overallStatistics: $this->calculateOverallStatistics($parseResults)
);
}
/**
* Analysiert ein einzelnes CSS-Verzeichnis
*/
public function analyzeDirectory(string $directory, bool $recursive = true): DesignSystemAnalysis
{
$parseResults = $this->cssParser->parseDirectory($directory, $recursive);
if (empty($parseResults)) {
throw new \InvalidArgumentException("No CSS files found in directory: $directory");
}
$combinedResult = $this->combineParseResults($parseResults);
return new DesignSystemAnalysis(
sourceFiles: array_map(fn ($result) => $result->sourceFile, $parseResults),
parseResults: $parseResults,
combinedResult: $combinedResult,
tokenAnalysis: $this->tokenAnalyzer->analyze($combinedResult),
componentAnalysis: $this->componentDetector->detectComponents($combinedResult),
conventionAnalysis: $this->conventionChecker->checkConventions($combinedResult),
colorAnalysis: $this->colorAnalyzer->analyzeColors($combinedResult),
overallStatistics: $this->calculateOverallStatistics($parseResults)
);
}
/**
* Schnelle Analyse nur für Statistiken
*/
public function quickAnalyze(string|array $cssFiles): array
{
$files = is_array($cssFiles) ? $cssFiles : [$cssFiles];
$statistics = [];
foreach ($files as $file) {
$filePath = $file instanceof FilePath ? $file : FilePath::create($file);
$result = $this->cssParser->parseFile($filePath);
$statistics[] = $result->getStatistics();
}
return $statistics;
}
/**
* Kombiniert mehrere Parse-Ergebnisse zu einem Gesamt-Ergebnis
*/
private function combineParseResults(array $parseResults): CssParseResult
{
if (empty($parseResults)) {
throw new \InvalidArgumentException('No parse results to combine');
}
if (count($parseResults) === 1) {
return $parseResults[0];
}
$allRules = [];
$allCustomProperties = [];
$allClassNames = [];
$combinedContent = '';
foreach ($parseResults as $result) {
$allRules = array_merge($allRules, $result->rules);
$allCustomProperties = array_merge($allCustomProperties, $result->customProperties);
$allClassNames = array_merge($allClassNames, $result->classNames);
$combinedContent .= $result->rawContent . "\n";
}
return new CssParseResult(
sourceFile: null, // Combined result has no single source
rules: $allRules,
customProperties: $allCustomProperties,
classNames: $allClassNames,
rawContent: $combinedContent
);
}
/**
* Berechnet Gesamt-Statistiken für alle Dateien
*/
private function calculateOverallStatistics(array $parseResults): array
{
$totalStats = [
'total_files' => count($parseResults),
'total_rules' => 0,
'total_selectors' => 0,
'total_properties' => 0,
'total_design_tokens' => 0,
'total_class_names' => 0,
'total_content_size' => 0,
'file_breakdown' => [],
];
foreach ($parseResults as $result) {
$stats = $result->getStatistics();
$totalStats['total_rules'] += $stats['total_rules'];
$totalStats['total_selectors'] += $stats['total_selectors'];
$totalStats['total_properties'] += $stats['total_properties'];
$totalStats['total_design_tokens'] += $stats['design_tokens'];
$totalStats['total_class_names'] += $stats['class_names'];
$totalStats['total_content_size'] += $stats['content_size_bytes'];
$totalStats['file_breakdown'][] = $stats;
}
// Durchschnittliche Werte berechnen
$fileCount = max(1, count($parseResults));
$totalStats['avg_rules_per_file'] = round($totalStats['total_rules'] / $fileCount, 2);
$totalStats['avg_properties_per_file'] = round($totalStats['total_properties'] / $fileCount, 2);
$totalStats['avg_tokens_per_file'] = round($totalStats['total_design_tokens'] / $fileCount, 2);
$totalStats['avg_classes_per_file'] = round($totalStats['total_class_names'] / $fileCount, 2);
return $totalStats;
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Analyzer;
/**
* Ergebnis der Token-Analyse
*/
final readonly class TokenAnalysisResult
{
public function __construct(
public int $totalTokens,
public array $tokensByType,
public array $tokenHierarchy,
public array $unusedTokens,
public array $missingTokens,
public array $tokenUsage,
public array $recommendations,
public array $usedTokens = []
) {
}
/**
* Gibt die häufigsten Token-Typen zurück
*/
public function getMostUsedTokenTypes(): array
{
$usage = [];
foreach ($this->tokensByType as $type => $data) {
$usage[$type] = $data['count'];
}
arsort($usage);
return $usage;
}
/**
* Gibt Token-Coverage-Statistiken zurück
*/
public function getTokenCoverage(): array
{
$usedTokens = count($this->tokenUsage);
$totalTokens = $this->totalTokens;
return [
'used_tokens' => $usedTokens,
'unused_tokens' => count($this->unusedTokens),
'total_tokens' => $totalTokens,
'usage_percentage' => $totalTokens > 0 ? round(($usedTokens / $totalTokens) * 100, 1) : 0,
];
}
/**
* Gibt Empfehlungs-Prioritäten zurück
*/
public function getRecommendationPriorities(): array
{
$priorities = [];
foreach ($this->recommendations as $recommendation) {
if (str_contains($recommendation, 'Remove')) {
$priorities[] = ['level' => 'high', 'text' => $recommendation];
} elseif (str_contains($recommendation, 'Consider adding')) {
$priorities[] = ['level' => 'medium', 'text' => $recommendation];
} else {
$priorities[] = ['level' => 'low', 'text' => $recommendation];
}
}
return $priorities;
}
/**
* Konvertiert zu Array für Export
*/
public function toArray(): array
{
return [
'total_tokens' => $this->totalTokens,
'tokens_by_type' => $this->tokensByType,
'token_hierarchy' => $this->tokenHierarchy,
'unused_tokens' => array_map(fn ($token) => $token->toArray(), $this->unusedTokens),
'missing_tokens' => $this->missingTokens,
'token_usage' => $this->tokenUsage,
'recommendations' => $this->recommendations,
'coverage' => $this->getTokenCoverage(),
'priorities' => $this->getRecommendationPriorities(),
];
}
}

View File

@@ -0,0 +1,320 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Analyzer;
use App\Framework\Design\ValueObjects\CssParseResult;
use App\Framework\Design\ValueObjects\DesignToken;
use App\Framework\Design\ValueObjects\DesignTokenType;
/**
* Analyzer für Design Tokens
*/
final readonly class TokenAnalyzer
{
/**
* Analysiert Design Tokens im CSS
*/
public function analyze(CssParseResult $parseResult): TokenAnalysisResult
{
$tokens = $parseResult->customProperties;
if (empty($tokens)) {
return new TokenAnalysisResult(
totalTokens: 0,
tokensByType: [],
tokenHierarchy: [],
unusedTokens: [],
missingTokens: [],
tokenUsage: [],
recommendations: ['No design tokens found. Consider implementing CSS custom properties for better maintainability.']
);
}
// Analysiere Token-Typen
$tokensByType = $this->groupTokensByType($tokens);
// Analysiere Token-Hierarchie
$tokenHierarchy = $this->analyzeTokenHierarchy($tokens);
// Analysiere Token-Usage
$tokenUsage = $this->analyzeTokenUsage($parseResult, $tokens);
// Finde unbenutzte Tokens
$unusedTokens = $this->findUnusedTokens($tokens, $tokenUsage);
// Finde verwendete Tokens (alle Tokens minus unbenutzte)
$usedTokens = array_filter($tokens, fn($token) => !in_array($token, $unusedTokens, true));
// Finde fehlende Standard-Tokens
$missingTokens = $this->findMissingStandardTokens($tokensByType);
// Generiere Empfehlungen
$recommendations = $this->generateRecommendations($tokensByType, $unusedTokens, $missingTokens);
return new TokenAnalysisResult(
totalTokens: count($tokens),
tokensByType: $tokensByType,
tokenHierarchy: $tokenHierarchy,
unusedTokens: $unusedTokens,
missingTokens: $missingTokens,
tokenUsage: $tokenUsage,
recommendations: $recommendations,
usedTokens: $usedTokens
);
}
/**
* Gruppiert Tokens nach Typ
*/
private function groupTokensByType(array $tokens): array
{
$grouped = [];
foreach ($tokens as $token) {
$type = $token->type->value;
if (! isset($grouped[$type])) {
$grouped[$type] = [
'count' => 0,
'tokens' => [],
];
}
$grouped[$type]['count']++;
$grouped[$type]['tokens'][] = $token;
}
return $grouped;
}
/**
* Analysiert Token-Hierarchie (basierend auf Naming)
*/
private function analyzeTokenHierarchy(array $tokens): array
{
$hierarchy = [];
foreach ($tokens as $token) {
$nameParts = explode('-', $token->name);
$current = &$hierarchy;
foreach ($nameParts as $part) {
if (! isset($current[$part])) {
$current[$part] = [
'tokens' => [],
'children' => [],
];
}
$current = &$current[$part]['children'];
}
// Token zur letzten Ebene hinzufügen
$lastPart = end($nameParts);
if (isset($hierarchy[$lastPart])) {
$hierarchy[$lastPart]['tokens'][] = $token;
}
}
return $this->cleanupHierarchy($hierarchy);
}
/**
* Analysiert Token-Usage in CSS Rules
*/
private function analyzeTokenUsage(CssParseResult $parseResult, array $tokens): array
{
$usage = [];
$tokenNames = array_map(fn (DesignToken $token) => $token->name, $tokens);
foreach ($parseResult->rules as $rule) {
foreach ($rule->properties as $property) {
if ($property->usesCustomProperty()) {
$refs = $property->getCustomPropertyReferences();
foreach ($refs as $ref) {
if (in_array($ref, $tokenNames)) {
if (! isset($usage[$ref])) {
$usage[$ref] = [
'count' => 0,
'properties' => [],
'selectors' => [],
];
}
$usage[$ref]['count']++;
$usage[$ref]['properties'][] = $property->name;
$usage[$ref]['selectors'] = array_merge(
$usage[$ref]['selectors'],
$rule->getSelectorStrings()
);
}
}
}
}
}
// Deduplicate arrays
foreach ($usage as $tokenName => $data) {
$usage[$tokenName]['properties'] = array_unique($data['properties']);
$usage[$tokenName]['selectors'] = array_unique($data['selectors']);
}
return $usage;
}
/**
* Findet unbenutzte Tokens
*/
private function findUnusedTokens(array $tokens, array $usage): array
{
$unused = [];
foreach ($tokens as $token) {
if (! isset($usage[$token->name])) {
$unused[] = $token;
}
}
return $unused;
}
/**
* Findet fehlende Standard-Tokens
*/
private function findMissingStandardTokens(array $tokensByType): array
{
$standardTokens = [
DesignTokenType::COLOR->value => [
'primary', 'secondary', 'success', 'warning', 'error', 'info',
'background', 'surface', 'text-primary', 'text-secondary',
],
DesignTokenType::SPACING->value => [
'xs', 'sm', 'md', 'lg', 'xl', '2xl',
'gap-xs', 'gap-sm', 'gap-md', 'gap-lg',
],
DesignTokenType::TYPOGRAPHY->value => [
'font-primary', 'font-secondary',
'text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl',
'weight-normal', 'weight-bold',
],
DesignTokenType::RADIUS->value => [
'radius-sm', 'radius-md', 'radius-lg', 'radius-full',
],
DesignTokenType::SHADOW->value => [
'shadow-sm', 'shadow-md', 'shadow-lg',
],
];
$missing = [];
foreach ($standardTokens as $type => $expectedTokens) {
if (! isset($tokensByType[$type])) {
$missing[$type] = $expectedTokens;
continue;
}
$existingNames = array_map(
fn (DesignToken $token) => $token->name,
$tokensByType[$type]['tokens']
);
$missingInType = array_diff($expectedTokens, $existingNames);
if (! empty($missingInType)) {
$missing[$type] = $missingInType;
}
}
return $missing;
}
/**
* Generiert Empfehlungen basierend auf der Analyse
*/
private function generateRecommendations(array $tokensByType, array $unusedTokens, array $missingTokens): array
{
$recommendations = [];
// Empfehlungen für unbenutzte Tokens
if (! empty($unusedTokens)) {
$recommendations[] = sprintf(
'Remove %d unused design tokens to reduce CSS complexity.',
count($unusedTokens)
);
}
// Empfehlungen für fehlende Tokens
if (! empty($missingTokens)) {
foreach ($missingTokens as $type => $tokens) {
$recommendations[] = sprintf(
'Consider adding %s tokens: %s',
$type,
implode(', ', array_slice($tokens, 0, 3)) . (count($tokens) > 3 ? '...' : '')
);
}
}
// Empfehlungen für Token-Balance
$totalTokens = array_sum(array_column($tokensByType, 'count'));
if ($totalTokens > 0) {
foreach ($tokensByType as $type => $data) {
$percentage = round(($data['count'] / $totalTokens) * 100, 1);
if ($type === 'color' && $percentage > 50) {
$recommendations[] = 'High percentage of color tokens detected. Consider consolidating similar colors.';
}
if ($type === 'spacing' && $percentage < 10 && $totalTokens > 10) {
$recommendations[] = 'Low percentage of spacing tokens. Consider adding more consistent spacing values.';
}
}
}
// Empfehlungen für Naming Conventions
if (isset($tokensByType[DesignTokenType::COLOR->value])) {
$colorTokens = $tokensByType[DesignTokenType::COLOR->value]['tokens'];
$semanticNames = 0;
$literalNames = 0;
foreach ($colorTokens as $token) {
if (preg_match('/^(primary|secondary|success|warning|error|info|background|surface|text)/', $token->name)) {
$semanticNames++;
} elseif (preg_match('/^(red|blue|green|yellow|orange|purple|gray|black|white)/', $token->name)) {
$literalNames++;
}
}
if ($literalNames > $semanticNames) {
$recommendations[] = 'Consider using more semantic color names (primary, secondary) instead of literal names (red, blue).';
}
}
if (empty($recommendations)) {
$recommendations[] = 'Design token structure looks good! Consider documenting token usage guidelines.';
}
return $recommendations;
}
/**
* Bereinigt die Hierarchie von leeren Einträgen
*/
private function cleanupHierarchy(array $hierarchy): array
{
$cleaned = [];
foreach ($hierarchy as $key => $value) {
if (! empty($value['tokens']) || ! empty($value['children'])) {
$cleaned[$key] = [
'tokens' => $value['tokens'],
'children' => $this->cleanupHierarchy($value['children']),
];
}
}
return $cleaned;
}
}