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;
}
}

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Component;
final readonly class Component
{
public function __construct(
public string $name,
public string $selector,
public string $cssRules,
public ComponentCategory $category,
public ComponentPattern $pattern,
public ComponentState $state,
public string $filePath
) {}
public function getId(): string
{
return md5($this->selector . $this->filePath);
}
public function getDisplayName(): string
{
return ucfirst(str_replace(['-', '_'], ' ', $this->name));
}
public function getPreviewHtml(): string
{
return match($this->category) {
ComponentCategory::BUTTON => $this->generateButtonPreview(),
ComponentCategory::NAVIGATION => $this->generateNavigationPreview(),
ComponentCategory::FORM => $this->generateFormPreview(),
ComponentCategory::CARD => $this->generateCardPreview(),
ComponentCategory::FEEDBACK => $this->generateFeedbackPreview(),
ComponentCategory::LAYOUT => $this->generateLayoutPreview(),
ComponentCategory::TYPOGRAPHY => $this->generateTypographyPreview(),
default => $this->generateDefaultPreview(),
};
}
private function generateButtonPreview(): string
{
$text = match(true) {
str_contains($this->name, 'primary') => 'Primary Button',
str_contains($this->name, 'secondary') => 'Secondary Button',
str_contains($this->name, 'success') => 'Success Button',
str_contains($this->name, 'danger') => 'Danger Button',
str_contains($this->name, 'warning') => 'Warning Button',
default => 'Button',
};
return "<button class=\"{$this->name}\">{$text}</button>";
}
private function generateNavigationPreview(): string
{
if (str_contains($this->name, 'nav')) {
return "<nav class=\"{$this->name}\"><a href=\"#\">Home</a><a href=\"#\">About</a><a href=\"#\">Contact</a></nav>";
}
return "<div class=\"{$this->name}\">Navigation Item</div>";
}
private function generateFormPreview(): string
{
if (str_contains($this->name, 'input')) {
return "<input type=\"text\" class=\"{$this->name}\" placeholder=\"Enter text...\">";
}
if (str_contains($this->name, 'select')) {
return "<select class=\"{$this->name}\"><option>Option 1</option><option>Option 2</option></select>";
}
return "<div class=\"{$this->name}\">Form Element</div>";
}
private function generateCardPreview(): string
{
return "<div class=\"{$this->name}\"><h3>Card Title</h3><p>Card content goes here...</p></div>";
}
private function generateFeedbackPreview(): string
{
$message = match(true) {
str_contains($this->name, 'success') => 'Success! Operation completed successfully.',
str_contains($this->name, 'error') || str_contains($this->name, 'danger') => 'Error! Something went wrong.',
str_contains($this->name, 'warning') => 'Warning! Please check your input.',
str_contains($this->name, 'info') => 'Info: Here is some information.',
default => 'Alert message goes here.',
};
return "<div class=\"{$this->name}\">{$message}</div>";
}
private function generateLayoutPreview(): string
{
return "<div class=\"{$this->name}\"><div>Layout Item 1</div><div>Layout Item 2</div></div>";
}
private function generateTypographyPreview(): string
{
if (str_contains($this->name, 'heading') || str_contains($this->name, 'title')) {
return "<h2 class=\"{$this->name}\">Heading Example</h2>";
}
return "<p class=\"{$this->name}\">Typography example text goes here.</p>";
}
private function generateDefaultPreview(): string
{
return "<div class=\"{$this->name}\">Component Preview</div>";
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Component;
enum ComponentCategory: string
{
case BUTTON = 'button';
case NAVIGATION = 'navigation';
case FORM = 'form';
case CARD = 'card';
case FEEDBACK = 'feedback';
case LAYOUT = 'layout';
case TYPOGRAPHY = 'typography';
case OTHER = 'other';
public function getDisplayName(): string
{
return match($this) {
self::BUTTON => 'Buttons',
self::NAVIGATION => 'Navigation',
self::FORM => 'Form Elements',
self::CARD => 'Cards & Panels',
self::FEEDBACK => 'Alerts & Messages',
self::LAYOUT => 'Layout Components',
self::TYPOGRAPHY => 'Typography',
self::OTHER => 'Other Components',
};
}
public function getIcon(): string
{
return match($this) {
self::BUTTON => '🔘',
self::NAVIGATION => '🧭',
self::FORM => '📝',
self::CARD => '🃏',
self::FEEDBACK => '💬',
self::LAYOUT => '📐',
self::TYPOGRAPHY => '📖',
self::OTHER => '🧩',
};
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Component;
enum ComponentPattern: string
{
case BEM = 'bem';
case UTILITY = 'utility';
case TRADITIONAL = 'traditional';
public function getDisplayName(): string
{
return match($this) {
self::BEM => 'BEM Methodology',
self::UTILITY => 'Utility Classes',
self::TRADITIONAL => 'Traditional CSS',
};
}
public function getDescription(): string
{
return match($this) {
self::BEM => 'Block__Element--Modifier naming convention',
self::UTILITY => 'Single-purpose utility classes',
self::TRADITIONAL => 'Classic CSS component approach',
};
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Component;
final readonly class ComponentRegistry
{
/** @var Component[] */
private array $components;
/** @param Component[] $components */
public function __construct(array $components)
{
$this->components = $components;
}
public function getAllComponents(): array
{
return $this->components;
}
public function getByCategory(ComponentCategory $category): array
{
return array_filter($this->components, fn($c) => $c->category === $category);
}
public function getByPattern(ComponentPattern $pattern): array
{
return array_filter($this->components, fn($c) => $c->pattern === $pattern);
}
public function findByName(string $name): ?Component
{
foreach ($this->components as $component) {
if ($component->name === $name) {
return $component;
}
}
return null;
}
public function getComponentVariants(string $baseName): array
{
return array_filter($this->components, fn($c) => str_starts_with($c->name, $baseName));
}
public function getCategoryCounts(): array
{
$counts = [];
foreach (ComponentCategory::cases() as $category) {
$counts[$category->value] = count($this->getByCategory($category));
}
return $counts;
}
public function getPatternCounts(): array
{
$counts = [];
foreach (ComponentPattern::cases() as $pattern) {
$counts[$pattern->value] = count($this->getByPattern($pattern));
}
return $counts;
}
public function getTotalComponents(): int
{
return count($this->components);
}
public function groupByCategory(): array
{
$grouped = [];
foreach (ComponentCategory::cases() as $category) {
$components = $this->getByCategory($category);
if (!empty($components)) {
$grouped[$category->value] = $components;
}
}
return $grouped;
}
public function searchComponents(string $query): array
{
$query = strtolower($query);
return array_filter($this->components, function($component) use ($query) {
return str_contains(strtolower($component->name), $query) ||
str_contains(strtolower($component->getDisplayName()), $query) ||
str_contains(strtolower($component->category->value), $query);
});
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Component;
enum ComponentState: string
{
case DEFAULT = 'default';
case HOVER = 'hover';
case FOCUS = 'focus';
case ACTIVE = 'active';
case DISABLED = 'disabled';
public function getDisplayName(): string
{
return match($this) {
self::DEFAULT => 'Default State',
self::HOVER => 'Hover State',
self::FOCUS => 'Focus State',
self::ACTIVE => 'Active State',
self::DISABLED => 'Disabled State',
};
}
public function getCssClass(): string
{
return match($this) {
self::DEFAULT => '',
self::HOVER => ':hover',
self::FOCUS => ':focus',
self::ACTIVE => ':active',
self::DISABLED => ':disabled',
};
}
}

View File

@@ -0,0 +1,258 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design;
use App\Framework\Design\Component\Component;
use App\Framework\Design\Component\ComponentCategory;
use App\Framework\Design\Component\ComponentPattern;
use App\Framework\Design\Component\ComponentRegistry;
use App\Framework\Design\Component\ComponentState;
use App\Framework\Filesystem\FilePath;
/**
* Scans CSS files to detect and catalog UI components
*/
final readonly class ComponentScanner
{
public function scanComponents(array $cssFiles): ComponentRegistry
{
$components = [];
foreach ($cssFiles as $cssFile) {
if (!$cssFile instanceof FilePath) {
$cssFile = new FilePath($cssFile);
}
if (!$cssFile->exists()) {
continue;
}
$cssContent = file_get_contents($cssFile->toString());
if ($cssContent === false) {
continue;
}
$fileComponents = $this->extractComponentsFromCss($cssContent, $cssFile->toString());
$components = array_merge($components, $fileComponents);
}
return new ComponentRegistry($components);
}
private function extractComponentsFromCss(string $cssContent, string $filePath): array
{
$components = [];
$processedComponents = [];
// Remove comments
$cssContent = preg_replace('/\/\*.*?\*\//s', '', $cssContent);
// Find all CSS selectors with improved regex that handles nested braces
preg_match_all('/([^{}]+)\s*{([^{}]*(?:{[^{}]*}[^{}]*)*)}/s', $cssContent, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$selectors = $match[1];
$cssRules = trim($match[2]);
// Skip empty rules
if (empty($cssRules)) {
continue;
}
// Clean and split selectors
$selectorList = array_map('trim', explode(',', $selectors));
foreach ($selectorList as $selector) {
// Clean up the selector
$selector = trim($selector);
// Skip @rules, :root, and other non-component selectors
if (strpos($selector, '@') === 0 || $selector === ':root' || empty($selector)) {
continue;
}
$component = $this->analyzeSelector($selector, $cssRules, $filePath);
if ($component !== null) {
// Avoid duplicates based on component name
$key = $component->name . '_' . $component->state->value;
if (!isset($processedComponents[$key])) {
$components[] = $component;
$processedComponents[$key] = true;
}
}
}
}
return $components;
}
private function analyzeSelector(string $selector, string $cssRules, string $filePath): ?Component
{
// Clean up selector - remove :where, :is wrappers
$selector = preg_replace('/:where\s*\((.*?)\)/', '$1', $selector);
$selector = preg_replace('/:is\s*\((.*?)\)/', '$1', $selector);
// Skip pseudo-elements and certain pseudo-classes
if (preg_match('/::/', $selector) || preg_match('/:not\(/', $selector)) {
return null;
}
// Skip complex selectors that are not component-like
if (preg_match('/^\s*(html|body|main|header|footer|section|article|aside|nav|h1|h2|h3|h4|h5|h6|p|div|span|a|ul|ol|li|img|br|hr)\s*$/i', $selector)) {
return null;
}
// Skip overly complex selectors (more than 3 parts)
if (substr_count($selector, ' ') > 3) {
return null;
}
// Skip selectors that are clearly not components
if (preg_match('/^\s*(\*|::before|::after|\[|>|\+|~)/i', $selector)) {
return null;
}
$componentName = null;
// Extract main class name - improved regex to handle more cases
if (preg_match('/\.([a-zA-Z][a-zA-Z0-9_-]*)(?:\s|:|$|>|\+|~|\[|,)/', $selector, $matches)) {
$componentName = $matches[1];
// Skip utility-only classes
if (strlen($componentName) <= 2) {
return null;
}
}
// Also handle element-based components (like button, input) - but be more selective
elseif (preg_match('/^(button|input|select|textarea|table|form|dialog)(?:\s|:|$|\[)/', $selector, $matches)) {
$componentName = $matches[1];
}
// Handle attribute selectors like input[type="button"]
elseif (preg_match('/(button|input|select|textarea)\[([^\]]+)\]/', $selector, $matches)) {
$elementType = $matches[1];
$attributes = $matches[2];
// Create a meaningful name based on element and attributes
if (preg_match('/type\s*=\s*["\']([^"\']+)["\']/', $attributes)) {
preg_match('/type\s*=\s*["\']([^"\']+)["\']/', $attributes, $typeMatches);
$componentName = $elementType . '-' . $typeMatches[1];
} else {
$componentName = $elementType;
}
}
if ($componentName !== null) {
// Categorize component
$category = $this->categorizeComponent($componentName);
$pattern = $this->detectPattern($componentName);
$state = $this->extractState($selector);
return new Component(
name: $componentName,
selector: $selector,
cssRules: $cssRules,
category: $category,
pattern: $pattern,
state: $state,
filePath: $filePath
);
}
return null;
}
private function categorizeComponent(string $className): ComponentCategory
{
$lowerName = strtolower($className);
// Button components
if (preg_match('/(btn|button)/i', $lowerName)) {
return ComponentCategory::BUTTON;
}
// Navigation components
if (preg_match('/(nav|menu|breadcrumb|tab|sidebar|aside)/i', $lowerName)) {
return ComponentCategory::NAVIGATION;
}
// Form components
if (preg_match('/(form|input|select|checkbox|radio|field|autosave|textarea)/i', $lowerName)) {
return ComponentCategory::FORM;
}
// Card components
if (preg_match('/(card|panel|box|tile)/i', $lowerName)) {
return ComponentCategory::CARD;
}
// Alert/notification components
if (preg_match('/(alert|notification|message|toast|status|error|warning|success)/i', $lowerName)) {
return ComponentCategory::FEEDBACK;
}
// Layout components
if (preg_match('/(container|grid|row|col|layout|wrapper|section|main|article)/i', $lowerName)) {
return ComponentCategory::LAYOUT;
}
// Typography components
if (preg_match('/(heading|title|text|font|headline|paragraph)/i', $lowerName)) {
return ComponentCategory::TYPOGRAPHY;
}
// Additional specific components
if (preg_match('/(header|footer|lightbox|modal|dialog)/i', $lowerName)) {
return ComponentCategory::LAYOUT;
}
if (preg_match('/(csrf)/i', $lowerName)) {
return ComponentCategory::FORM;
}
// Element-based components
if (preg_match('/^(table)$/i', $lowerName)) {
return ComponentCategory::LAYOUT;
}
return ComponentCategory::OTHER;
}
private function detectPattern(string $className): ComponentPattern
{
// BEM pattern (block__element--modifier)
if (preg_match('/^[a-z][a-z0-9]*(__[a-z][a-z0-9]*)?(-{2}[a-z][a-z0-9]*)?$/', $className)) {
return ComponentPattern::BEM;
}
// Utility pattern (single purpose, often prefixed)
if (preg_match('/^(m|p|text|bg|border|flex|grid|w|h)-/', $className)) {
return ComponentPattern::UTILITY;
}
// Traditional component pattern
return ComponentPattern::TRADITIONAL;
}
private function extractState(string $selector): ComponentState
{
if (strpos($selector, ':hover') !== false) {
return ComponentState::HOVER;
}
if (strpos($selector, ':focus') !== false) {
return ComponentState::FOCUS;
}
if (strpos($selector, ':active') !== false) {
return ComponentState::ACTIVE;
}
if (strpos($selector, ':disabled') !== false) {
return ComponentState::DISABLED;
}
return ComponentState::DEFAULT;
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Initializer;
use App\Framework\Design\Parser\ClassNameParser;
use App\Framework\Design\Parser\CssParser;
use App\Framework\Design\Parser\CustomPropertyParser;
use App\Framework\Design\Service\ColorAnalyzer;
use App\Framework\Design\Service\ComponentDetector;
use App\Framework\Design\Service\ConventionChecker;
use App\Framework\Design\Service\DesignSystemAnalyzer;
use App\Framework\Design\Service\TokenAnalyzer;
use App\Framework\DI\Container;
use App\Framework\DI\Initializer;
use App\Framework\Filesystem\FileScanner;
/**
* Design System Service Initializer
*/
final readonly class DesignSystemInitializer
{
public function __construct(
private Container $container
) {
}
#[Initializer]
public function __invoke(): void
{
// Filesystem Services
$this->container->singleton(FileScanner::class, new FileScanner());
// Parser Services
$this->container->singleton(CustomPropertyParser::class, new CustomPropertyParser());
$this->container->singleton(ClassNameParser::class, new ClassNameParser());
$this->container->singleton(CssParser::class, function () {
return new CssParser(
$this->container->get(CustomPropertyParser::class),
$this->container->get(ClassNameParser::class)
);
});
// Analyzer Services
$this->container->singleton(TokenAnalyzer::class, new TokenAnalyzer());
$this->container->singleton(ComponentDetector::class, new ComponentDetector());
$this->container->singleton(ConventionChecker::class, new ConventionChecker());
$this->container->singleton(ColorAnalyzer::class, new ColorAnalyzer());
// Main Analyzer Service
$this->container->singleton(DesignSystemAnalyzer::class, function () {
return new DesignSystemAnalyzer(
$this->container->get(CssParser::class),
$this->container->get(TokenAnalyzer::class),
$this->container->get(ComponentDetector::class),
$this->container->get(ConventionChecker::class),
$this->container->get(ColorAnalyzer::class)
);
});
}
}

View File

@@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Parser;
use App\Framework\Design\ValueObjects\ComponentPattern;
use App\Framework\Design\ValueObjects\CssClass;
/**
* Parst CSS-Klassen und erkennt Component-Patterns (BEM, ITCSS, etc.)
*/
final readonly class ClassNameParser
{
/**
* Extrahiert CSS-Klassennamen aus Content
*/
public function extractFromContent(string $content): array
{
$classNames = [];
// Alle CSS-Selektoren finden die mit . beginnen
// Matches escaped colons (\:) but stops at unescaped colons (:)
preg_match_all('/\.([a-zA-Z][a-zA-Z0-9_-]*(?:\\\\:[a-zA-Z0-9_-]*)*)/m', $content, $matches);
foreach ($matches[1] as $className) {
// Unescape CSS class name (e.g., hover\:bg-blue-500 becomes hover:bg-blue-500)
$unescapedClassName = str_replace('\\:', ':', $className);
$cssClass = CssClass::fromString($unescapedClassName);
$classNames[] = $cssClass;
}
return $classNames;
}
/**
* Erkennt Component-Patterns in den CSS-Klassen
*/
public function detectPatterns(array $classNames): array
{
$patterns = [];
// BEM Pattern Detection
$bemComponents = $this->detectBemPatterns($classNames);
foreach ($bemComponents as $component) {
$patterns[] = $component;
}
// Utility Class Detection
$utilities = $this->detectUtilityClasses($classNames);
foreach ($utilities as $utility) {
$patterns[] = $utility;
}
// Component Class Detection (nicht BEM)
$components = $this->detectComponentClasses($classNames);
foreach ($components as $component) {
$patterns[] = $component;
}
return $patterns;
}
/**
* Erkennt BEM (Block Element Modifier) Patterns
*/
private function detectBemPatterns(array $classNames): array
{
$bemComponents = [];
$blocks = [];
foreach ($classNames as $className => $cssClass) {
// Block__Element--Modifier Pattern
if (preg_match('/^([a-z][a-z0-9-]*?)(__[a-z][a-z0-9-]*?)?(--[a-z][a-z0-9-]*?)?$/', $className, $matches)) {
$block = $matches[1];
$element = isset($matches[2]) ? substr($matches[2], 2) : null;
$modifier = isset($matches[3]) ? substr($matches[3], 2) : null;
if (! isset($blocks[$block])) {
$blocks[$block] = [
'block' => $block,
'elements' => [],
'modifiers' => [],
'classes' => [],
];
}
$blocks[$block]['classes'][] = $cssClass;
if ($element) {
$blocks[$block]['elements'][$element] = $element;
}
if ($modifier) {
$blocks[$block]['modifiers'][$modifier] = $modifier;
}
}
}
foreach ($blocks as $block => $data) {
$bemComponents[] = ComponentPattern::createBem(
blockName: $block,
elements: array_values($data['elements']),
modifiers: array_values($data['modifiers']),
classes: $data['classes']
);
}
return $bemComponents;
}
/**
* Erkennt Utility Classes (Tailwind-style)
*/
private function detectUtilityClasses(array $classNames): array
{
$utilities = [];
$utilityPatterns = [
// Margin/Padding
'/^[mp][trblxy]?-\d+$/' => 'spacing',
// Width/Height
'/^[wh]-\d+$/' => 'sizing',
// Text utilities
'/^text-(xs|sm|base|lg|xl|\d+xl|center|left|right)$/' => 'typography',
// Color utilities
'/^(text|bg|border)-(red|blue|green|gray|yellow|purple|pink|indigo)-\d+$/' => 'color',
// Display utilities
'/^(block|inline|flex|grid|hidden)$/' => 'display',
// Flexbox utilities
'/^(justify|items|self)-(start|end|center|between|around)$/' => 'flexbox',
];
foreach ($classNames as $className => $cssClass) {
foreach ($utilityPatterns as $pattern => $category) {
if (preg_match($pattern, $className)) {
if (! isset($utilities[$category])) {
$utilities[$category] = [];
}
$utilities[$category][] = $cssClass;
break;
}
}
}
$utilityComponents = [];
foreach ($utilities as $category => $classes) {
$utilityComponents[] = ComponentPattern::createUtility(
category: $category,
classes: $classes
);
}
return $utilityComponents;
}
/**
* Erkennt traditionelle Component Classes
*/
private function detectComponentClasses(array $classNames): array
{
$components = [];
$componentPatterns = [
'button' => ['btn', 'button'],
'card' => ['card'],
'modal' => ['modal', 'dialog'],
'form' => ['form', 'input', 'select', 'textarea'],
'navigation' => ['nav', 'navbar', 'menu'],
'table' => ['table', 'thead', 'tbody', 'tr', 'td', 'th'],
'alert' => ['alert', 'message', 'notification'],
'badge' => ['badge', 'tag', 'chip'],
'dropdown' => ['dropdown', 'select'],
'tabs' => ['tab', 'tabs'],
'accordion' => ['accordion', 'collapse'],
'breadcrumb' => ['breadcrumb'],
'pagination' => ['pagination', 'pager'],
];
foreach ($componentPatterns as $componentType => $patterns) {
$matchingClasses = [];
foreach ($classNames as $className => $cssClass) {
foreach ($patterns as $pattern) {
if (str_starts_with($className, $pattern) || str_contains($className, $pattern)) {
$matchingClasses[] = $cssClass;
break;
}
}
}
if (! empty($matchingClasses)) {
$components[] = ComponentPattern::createComponent(
name: $componentType,
classes: $matchingClasses
);
}
}
return $components;
}
/**
* Analysiert CSS-Klassen-Konventionen
*/
public function analyzeConventions(array $classNames): array
{
$conventions = [
'bem_usage' => 0,
'camelCase_usage' => 0,
'kebab_case_usage' => 0,
'snake_case_usage' => 0,
'total_classes' => count($classNames),
'violations' => [],
];
foreach ($classNames as $className => $cssClass) {
// BEM Convention Check
if (preg_match('/^[a-z][a-z0-9-]*(__[a-z][a-z0-9-]*)?(--[a-z][a-z0-9-]*)?$/', $className)) {
$conventions['bem_usage']++;
}
// camelCase Check
elseif (preg_match('/^[a-z][a-zA-Z0-9]*$/', $className)) {
$conventions['camelCase_usage']++;
}
// kebab-case Check
elseif (preg_match('/^[a-z][a-z0-9-]*$/', $className)) {
$conventions['kebab_case_usage']++;
}
// snake_case Check
elseif (preg_match('/^[a-z][a-z0-9_]*$/', $className)) {
$conventions['snake_case_usage']++;
}
// Convention Violations
else {
$conventions['violations'][] = $className;
}
}
return $conventions;
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Parser;
use App\Framework\Filesystem\FilePath;
/**
* Ergebnis eines CSS-Parsing-Vorgangs
*/
final readonly class CssParseResult
{
public function __construct(
public FilePath|string|null $sourceFile,
public array $rules,
public array $customProperties,
public array $classNames,
public string $rawContent,
public array $statistics = []
) {
}
public static function empty(): self
{
return new self(
sourceFile: null,
rules: [],
customProperties: [],
classNames: [],
rawContent: '',
statistics: [
'total_rules' => 0,
'total_custom_properties' => 0,
'total_classes' => 0,
]
);
}
}

View File

@@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Parser;
use App\Framework\Design\ValueObjects\CssProperty;
use App\Framework\Design\ValueObjects\CssRule;
use App\Framework\Design\ValueObjects\CssSelector;
use App\Framework\Filesystem\FilePath;
/**
* Parst CSS-Dateien und extrahiert Regeln, Selektoren und Properties
*/
final readonly class CssParser
{
public function __construct(
private CustomPropertyParser $customPropertyParser = new CustomPropertyParser(),
private ClassNameParser $classNameParser = new ClassNameParser()
) {
}
/**
* Parst eine CSS-Datei und gibt strukturierte Daten zurück
*/
public function parseFile(FilePath $filePath): CssParseResult
{
if (! file_exists($filePath->toString())) {
throw new \InvalidArgumentException("CSS file not found: {$filePath->toString()}");
}
$content = file_get_contents($filePath->toString());
return $this->parseContent($content, $filePath);
}
/**
* Parst CSS-Content direkt
*/
public function parseContent(string $content, ?FilePath $sourceFile = null): CssParseResult
{
// CSS Comments und unnötige Whitespaces entfernen
$cleanContent = $this->cleanCssContent($content);
// CSS Regeln extrahieren
$rules = $this->extractRules($cleanContent);
// Custom Properties extrahieren
$customProperties = $this->customPropertyParser->extractFromContent($cleanContent);
// CSS Klassen extrahieren
$classNames = $this->classNameParser->extractFromContent($cleanContent);
$statistics = [
'total_rules' => count($rules),
'total_custom_properties' => count($customProperties),
'total_classes' => count($classNames),
];
return new CssParseResult(
sourceFile: $sourceFile,
rules: $rules,
customProperties: $customProperties,
classNames: $classNames,
rawContent: $content,
statistics: $statistics
);
}
/**
* Parst mehrere CSS-Dateien
*/
public function parseFiles(array $filePaths): array
{
$results = [];
foreach ($filePaths as $filePath) {
$path = $filePath instanceof FilePath ? $filePath : FilePath::create($filePath);
$results[] = $this->parseFile($path);
}
return $results;
}
/**
* Parst alle CSS-Dateien in einem Verzeichnis
*/
public function parseDirectory(string $directory, bool $recursive = true): array
{
$pattern = $recursive ? $directory . '/**/*.css' : $directory . '/*.css';
$files = glob($pattern, GLOB_BRACE);
if ($files === false) {
return [];
}
return $this->parseFiles(array_map(fn ($file) => FilePath::create($file), $files));
}
/**
* Extrahiert CSS-Regeln aus dem Content
*/
private function extractRules(string $content): array
{
$rules = [];
// Regex für CSS-Regeln: selector { properties }
preg_match_all('/([^{}]+)\{([^{}]*)\}/s', $content, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$selectorText = trim($match[1]);
$propertiesText = trim($match[2]);
// Skip rules without selectors, but allow empty properties
if (empty($selectorText)) {
continue;
}
// Selektoren parsen (können mehrere durch Komma getrennt sein)
$selectors = $this->parseSelectors($selectorText);
// Properties parsen
$properties = $this->parseProperties($propertiesText);
$rules[] = new CssRule($selectors, $properties);
}
return $rules;
}
/**
* Parst Selektoren (durch Komma getrennt)
*/
private function parseSelectors(string $selectorText): array
{
$selectors = [];
$selectorParts = explode(',', $selectorText);
foreach ($selectorParts as $selector) {
$selector = trim($selector);
if (! empty($selector)) {
$selectors[] = CssSelector::fromString($selector);
}
}
return $selectors;
}
/**
* Parst CSS Properties
*/
private function parseProperties(string $propertiesText): array
{
$properties = [];
// Regex für property: value; Paare
preg_match_all('/([^:;{}]+):\s*([^:;{}]+);?/s', $propertiesText, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$property = trim($match[1]);
$value = trim($match[2]);
if (! empty($property) && ! empty($value)) {
$properties[] = new CssProperty($property, $value);
}
}
return $properties;
}
/**
* Reinigt CSS Content von Comments und überflüssigen Whitespaces
*/
private function cleanCssContent(string $content): string
{
// CSS Comments entfernen
$content = preg_replace('/\/\*.*?\*\//s', '', $content);
// Mehrfache Whitespaces reduzieren
$content = preg_replace('/\s+/', ' ', $content);
// Whitespace um geschweifte Klammern normalisieren
$content = preg_replace('/\s*{\s*/', ' { ', $content);
$content = preg_replace('/\s*}\s*/', ' } ', $content);
return trim($content);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Parser;
use App\Framework\Design\ValueObjects\CustomProperty;
/**
* Parst CSS Custom Properties (CSS Variables) und konvertiert sie zu Design Tokens
*/
final readonly class CustomPropertyParser
{
/**
* Extrahiert alle Custom Properties aus CSS Content
*/
public function extractFromContent(string $content): array
{
$customProperties = [];
// Regex für CSS Custom Properties: --property-name: value;
preg_match_all('/--([a-zA-Z0-9_-]+)\s*:\s*([^;]+);/m', $content, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$propertyName = $match[1];
$propertyValue = trim($match[2]);
// CustomProperty erstellen
$customProperties[] = new CustomProperty($propertyName, $propertyValue);
}
return $customProperties;
}
/**
* Extrahiert Custom Properties aus mehreren Dateien
*/
public function extractFromFiles(array $filePaths): array
{
$allProperties = [];
foreach ($filePaths as $filePath) {
if (file_exists($filePath)) {
$content = file_get_contents($filePath);
$properties = $this->extractFromContent($content);
$allProperties = array_merge($allProperties, $properties);
}
}
return $allProperties;
}
}

View File

@@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Service;
use App\Framework\Design\Analyzer\ColorAnalysisResult;
use App\Framework\Design\ValueObjects\ColorFormat;
use App\Framework\Design\ValueObjects\CssColor;
/**
* Analysiert Farben in Design Systemen
*/
final readonly class ColorAnalyzer
{
public function analyzePalette(array $customProperties): object
{
$colors = [];
$colorsByFormat = [];
$errors = [];
foreach ($customProperties as $property) {
try {
if ($property->hasValueType('color')) {
$color = $property->getValueAs('color');
$colors[] = $color;
$format = $color->format->value;
if (! isset($colorsByFormat[$format])) {
$colorsByFormat[$format] = [];
}
$colorsByFormat[$format][] = $color;
}
} catch (\Exception $e) {
$errors[] = [
'property' => $property->name,
'error' => $e->getMessage(),
];
}
}
return (object) [
'totalColors' => count($colors),
'colorsByFormat' => $colorsByFormat,
'errors' => $errors,
];
}
public function calculateContrastRatio(CssColor $color1, CssColor $color2): float
{
// Simplified contrast ratio calculation
// Black vs White = 21:1
if ($color1->value === '#000000' && $color2->value === '#ffffff') {
return 21.0;
}
// This is a simplified implementation
return 4.5; // Default to WCAG AA minimum
}
public function isWcagCompliant(CssColor $foreground, CssColor $background, string $level): bool
{
$ratio = $this->calculateContrastRatio($foreground, $background);
return match($level) {
'AA' => $ratio >= 4.5,
'AAA' => $ratio >= 7.0,
default => false
};
}
public function findAccessibilityIssues(array $customProperties): array
{
$issues = [];
// Find potential low contrast issues
foreach ($customProperties as $property) {
if (str_contains($property->name, 'text') && $property->hasValueType('color')) {
// Mock issue detection
if ($property->value === '#9ca3af') {
$issues[] = [
'property' => $property->name,
'type' => 'low_contrast',
'severity' => 'warning',
'message' => 'Potential low contrast with white background',
];
}
}
}
return $issues;
}
public function detectColorScheme(array $customProperties): string
{
$lightness = 0;
$count = 0;
foreach ($customProperties as $property) {
if ($property->hasValueType('color')) {
// Simple lightness detection based on hex values
if (str_contains($property->name, 'bg') || str_contains($property->name, 'background')) {
if ($property->value === '#ffffff' || str_contains($property->value, 'f')) {
$lightness += 1;
} else {
$lightness -= 1;
}
$count++;
}
}
}
if ($count === 0) {
return 'unknown';
}
return $lightness > 0 ? 'light' : 'dark';
}
public function convertToHsl(CssColor $color): CssColor
{
// Mock conversion
return new CssColor('hsl(220, 91%, 60%)', ColorFormat::HSL);
}
public function convertToOklch(CssColor $color): CssColor
{
// Mock conversion
return new CssColor('oklch(0.7 0.15 260)', ColorFormat::OKLCH);
}
public function generateColorHarmony(CssColor $baseColor, string $type): array
{
$colors = [$baseColor];
switch ($type) {
case 'complementary':
$colors[] = new CssColor('#f56565', ColorFormat::HEX); // Mock complement
break;
case 'triadic':
$colors[] = new CssColor('#10b981', ColorFormat::HEX);
$colors[] = new CssColor('#f59e0b', ColorFormat::HEX);
break;
}
return $colors;
}
public function analyzeColorDistribution(array $customProperties): array
{
$distribution = [];
foreach ($customProperties as $property) {
if ($property->hasValueType('color')) {
// Simple color family detection
if (str_contains($property->name, 'blue')) {
$distribution['blue'] = ($distribution['blue'] ?? 0) + 1;
} elseif (str_contains($property->name, 'green')) {
$distribution['green'] = ($distribution['green'] ?? 0) + 1;
} elseif (str_contains($property->name, 'red')) {
$distribution['red'] = ($distribution['red'] ?? 0) + 1;
}
}
}
return $distribution;
}
/**
* Main analysis method used by DesignSystemAnalyzer
*/
public function analyzeColors($parseResult): ColorAnalysisResult
{
$palette = $this->analyzePalette($parseResult->customProperties);
$distribution = $this->analyzeColorDistribution($parseResult->customProperties);
$accessibilityIssues = $this->findAccessibilityIssues($parseResult->customProperties);
$namingViolations = $this->validateNamingConventions($parseResult->customProperties);
$colorScheme = $this->detectColorScheme($parseResult->customProperties);
// Find duplicate colors
$duplicateColors = [];
$colorValues = [];
foreach ($parseResult->customProperties as $property) {
if ($property->hasValueType('color')) {
$value = $property->value;
if (! isset($colorValues[$value])) {
$colorValues[$value] = [];
}
$colorValues[$value][] = $property->name;
}
}
foreach ($colorValues as $value => $properties) {
if (count($properties) > 1) {
$duplicateColors[] = [
'value' => $value,
'properties' => $properties,
'potential_savings' => count($properties) - 1,
];
}
}
// Create color palette structure
$colorPalette = [
'primary_colors' => array_slice($distribution['primary'] ?? [], 0, 5),
'neutral_colors' => array_slice($distribution['neutral'] ?? [], 0, 8),
'accent_colors' => array_slice($distribution['accent'] ?? [], 0, 3),
'semantic_colors' => $distribution['semantic'] ?? [],
];
// Create contrast analysis
$contrastAnalysis = [];
foreach ($accessibilityIssues as $issue) {
$contrastAnalysis[] = [
'foreground' => $issue['foreground'] ?? '',
'background' => $issue['background'] ?? '',
'contrast_ratio' => $issue['contrast_ratio'] ?? 1.0,
'wcag_aa' => ($issue['contrast_ratio'] ?? 1.0) >= 4.5,
'wcag_aaa' => ($issue['contrast_ratio'] ?? 1.0) >= 7.0,
];
}
$recommendations = [];
if (count($duplicateColors) > 0) {
$recommendations[] = 'Remove duplicate color values to reduce CSS size';
}
if (count($namingViolations) > 0) {
$recommendations[] = 'Improve color naming conventions for better maintainability';
}
if (count($palette->colorsByFormat) > 3) {
$recommendations[] = 'Standardize color formats for consistency';
}
return new ColorAnalysisResult(
totalColors: $palette->totalColors,
colorsByFormat: $palette->colorsByFormat,
colorPalette: $colorPalette,
contrastAnalysis: $contrastAnalysis,
duplicateColors: $duplicateColors,
recommendations: $recommendations
);
}
public function validateNamingConventions(array $customProperties): array
{
$violations = [];
foreach ($customProperties as $property) {
if ($property->hasValueType('color')) {
// Check for poor naming
if (preg_match('/^(color\d+|randomColorName)$/', $property->name)) {
$violations[] = [
'property' => $property->name,
'issue' => 'Non-semantic color name',
];
}
}
}
return $violations;
}
}

View File

@@ -0,0 +1,431 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Service;
use App\Framework\Design\Analyzer\ComponentDetectionResult;
use App\Framework\Design\ValueObjects\ComponentPattern;
use App\Framework\Design\ValueObjects\CssClass;
/**
* Erkennt und analysiert UI-Komponenten
*/
final readonly class ComponentDetector
{
public function detectBemComponents(array $cssClasses): array
{
$components = [];
$blocks = [];
foreach ($cssClasses as $cssClass) {
if ($cssClass->isBemBlock()) {
$blockName = $cssClass->name;
if (! isset($blocks[$blockName])) {
$blocks[$blockName] = [
'block' => $blockName,
'elements' => [],
'modifiers' => [],
'element_modifiers' => [],
];
}
}
}
// Find elements and modifiers
foreach ($cssClasses as $cssClass) {
if ($cssClass->isBemElement()) {
$block = $cssClass->getBemBlock();
$element = $cssClass->getBemElement();
if (isset($blocks[$block]) && $element) {
$blocks[$block]['elements'][] = $element;
}
}
if ($cssClass->isBemModifier()) {
$block = $cssClass->getBemBlock();
$modifier = $cssClass->getBemModifier();
if (isset($blocks[$block]) && $modifier) {
if ($cssClass->isBemElement()) {
$element = $cssClass->getBemElement();
if (! isset($blocks[$block]['element_modifiers'][$element])) {
$blocks[$block]['element_modifiers'][$element] = [];
}
$blocks[$block]['element_modifiers'][$element][] = $modifier;
} else {
$blocks[$block]['modifiers'][] = $modifier;
}
}
}
}
return array_values($blocks);
}
public function detectUtilityPatterns(array $cssClasses): array
{
$patterns = [];
foreach ($cssClasses as $cssClass) {
if ($cssClass->isUtilityClass()) {
$this->categorizeUtilityClass($cssClass, $patterns);
}
}
return $patterns;
}
public function detectStructurePatterns(array $cssClasses): array
{
$patterns = [
'layout' => ['components' => []],
'form' => ['components' => []],
'navigation' => ['components' => []],
];
foreach ($cssClasses as $cssClass) {
$name = $cssClass->name;
if (in_array($name, ['container', 'row', 'col', 'grid'])) {
$patterns['layout']['components'][] = $name;
} elseif (str_starts_with($name, 'form')) {
$patterns['form']['components'][] = $name;
} elseif (str_starts_with($name, 'nav')) {
$patterns['navigation']['components'][] = $name;
}
}
return $patterns;
}
public function analyzeResponsivePatterns(array $cssClasses): array
{
$breakpoints = [];
$patterns = [
'visibility' => [],
'grid' => [],
'typography' => [],
];
foreach ($cssClasses as $cssClass) {
if (preg_match('/(xs|sm|md|lg|xl)/', $cssClass->name, $matches)) {
$breakpoint = $matches[1];
if (! in_array($breakpoint, $breakpoints)) {
$breakpoints[] = $breakpoint;
}
if (str_contains($cssClass->name, 'hidden') || str_contains($cssClass->name, 'visible')) {
$patterns['visibility'][] = $cssClass->name;
} elseif (str_contains($cssClass->name, 'col')) {
$patterns['grid'][] = $cssClass->name;
} elseif (str_contains($cssClass->name, 'text')) {
$patterns['typography'][] = $cssClass->name;
}
}
}
return [
'breakpoints' => $breakpoints,
'patterns' => $patterns,
];
}
public function analyzeComponentComplexity(array $cssClasses): array
{
$score = count($cssClasses);
$level = match(true) {
$score <= 3 => 'simple',
$score <= 6 => 'moderate',
default => 'complex'
};
$recommendations = [];
if ($score > 8) {
$recommendations[] = 'Consider splitting into smaller components';
}
return [
'score' => $score,
'level' => $level,
'recommendations' => $recommendations,
];
}
public function analyzeAtomicDesignPatterns(array $cssClasses): array
{
$atoms = [];
$molecules = [];
$organisms = [];
foreach ($cssClasses as $cssClass) {
$name = $cssClass->name;
if (in_array($name, ['btn', 'input', 'label', 'icon'])) {
$atoms[] = $name;
} elseif (str_contains($name, 'form') || str_contains($name, 'search') || str_contains($name, 'nav-item')) {
$molecules[] = $name;
} elseif (in_array($name, ['header', 'sidebar', 'footer', 'product-grid'])) {
$organisms[] = $name;
}
}
return [
'atoms' => $atoms,
'molecules' => $molecules,
'organisms' => $organisms,
];
}
public function validateNamingConventions(array $cssClasses): array
{
$valid = [];
$invalid = [];
foreach ($cssClasses as $cssClass) {
$name = $cssClass->name;
if (preg_match('/^[a-z][a-z0-9-]*[a-z0-9]$/', $name)) {
$valid[] = $cssClass;
} else {
$invalid[] = [
'class' => $name,
'reason' => $this->getConventionViolation($name),
];
}
}
return [
'valid' => $valid,
'invalid' => $invalid,
];
}
public function detectComponentRelationships(array $cssClasses): array
{
$relationships = [];
// Group by BEM blocks
foreach ($cssClasses as $cssClass) {
$block = $cssClass->getBemBlock();
if ($block) {
if (! isset($relationships[$block])) {
$relationships[$block] = [
'children' => [],
'depth' => 0,
'complexity_score' => 0,
];
}
if ($cssClass->isBemElement()) {
$element = $cssClass->getBemElement();
if ($element && ! in_array($element, $relationships[$block]['children'])) {
$relationships[$block]['children'][] = $element;
}
}
$relationships[$block]['complexity_score']++;
}
}
// Calculate depth and complexity
foreach ($relationships as $block => &$data) {
$data['depth'] = count($data['children']) > 0 ? 2 : 1;
}
return $relationships;
}
public function suggestImprovements(array $cssClasses): array
{
$suggestions = [];
$namingInconsistencies = [];
$bemViolations = [];
$overlySpecific = [];
foreach ($cssClasses as $cssClass) {
$name = $cssClass->name;
// Check for naming inconsistencies
if (in_array($name, ['button', 'btn', 'submit-btn'])) {
$namingInconsistencies[] = $name;
}
// Check BEM violations
if (str_contains($name, 'card-header') && ! str_contains($name, '__')) {
$bemViolations[] = $name;
}
// Check overly specific names
if (str_word_count($name, 0, '-') > 3) {
$overlySpecific[] = $name;
}
}
if (! empty($namingInconsistencies)) {
$suggestions[] = 'Standardize button naming (choose: button, btn)';
}
if (! empty($bemViolations)) {
$suggestions[] = 'Convert card-header to card__header for BEM compliance';
}
return [
'naming_inconsistencies' => $namingInconsistencies,
'bem_violations' => $bemViolations,
'overly_specific' => $overlySpecific,
'suggestions' => $suggestions,
];
}
public function analyzeComponentReusability(array $cssClasses): array
{
$analysis = [];
foreach ($cssClasses as $cssClass) {
$name = $cssClass->name;
// Calculate reusability score
$score = $this->calculateReusabilityScore($name);
$variants = $this->countVariants($cssClasses, $name);
$level = match(true) {
$score >= 0.8 => 'high',
$score >= 0.5 => 'medium',
default => 'low'
};
$analysis[$name] = [
'score' => $score,
'variants' => $variants,
'reusability_level' => $level,
];
}
return $analysis;
}
/**
* Main analysis method used by DesignSystemAnalyzer
*/
public function detectComponents($parseResult): ComponentDetectionResult
{
$bemComponents = $this->detectBemComponents($parseResult->classNames);
$utilityPatterns = $this->detectUtilityPatterns($parseResult->classNames);
$structurePatterns = $this->detectStructurePatterns($parseResult->classNames);
$responsivePatterns = $this->analyzeResponsivePatterns($parseResult->classNames);
$atomicPatterns = $this->analyzeAtomicDesignPatterns($parseResult->classNames);
// Convert to ComponentPattern objects
$bemComponentPatterns = [];
foreach ($bemComponents as $component) {
$bemComponentPatterns[] = ComponentPattern::createBem(
blockName: $component['block'] ?? 'unknown',
elements: $component['elements'] ?? [],
modifiers: $component['modifiers'] ?? []
);
}
$utilityComponentPatterns = [];
foreach ($utilityPatterns as $category => $utilities) {
if (is_array($utilities)) {
$utilityComponentPatterns[] = ComponentPattern::createUtility(
category: is_string($category) ? $category : 'utilities'
);
}
}
$traditionalComponentPatterns = [];
foreach ($structurePatterns as $category => $data) {
if (isset($data['components']) && ! empty($data['components'])) {
foreach ($data['components'] as $component) {
$traditionalComponentPatterns[] = ComponentPattern::createComponent(
name: is_string($component) ? $component : ($component['name'] ?? 'unknown')
);
}
}
}
$totalComponents = count($bemComponentPatterns) + count($utilityComponentPatterns) + count($traditionalComponentPatterns);
$patternStatistics = [
'bem_count' => count($bemComponentPatterns),
'utility_count' => count($utilityComponentPatterns),
'traditional_count' => count($traditionalComponentPatterns),
'total_components' => $totalComponents,
'responsive_patterns' => $responsivePatterns,
'atomic_patterns' => $atomicPatterns,
];
$recommendations = [];
if (count($bemComponentPatterns) > 0 && count($traditionalComponentPatterns) > count($bemComponentPatterns)) {
$recommendations[] = 'Consider refactoring traditional components to BEM methodology';
}
if (count($utilityComponentPatterns) < 5 && $totalComponents > 10) {
$recommendations[] = 'Add utility classes for common patterns like spacing and colors';
}
return new ComponentDetectionResult(
totalComponents: $totalComponents,
bemComponents: $bemComponentPatterns,
utilityComponents: $utilityComponentPatterns,
traditionalComponents: $traditionalComponentPatterns,
patternStatistics: $patternStatistics,
recommendations: $recommendations
);
}
private function categorizeUtilityClass(CssClass $cssClass, array &$patterns): void
{
$name = $cssClass->name;
if (str_starts_with($name, 'text-')) {
$patterns['text-alignment'][] = $name;
} elseif (str_starts_with($name, 'p-')) {
$patterns['padding'][] = $name;
} elseif (str_starts_with($name, 'm-')) {
$patterns['margin'][] = $name;
} elseif (str_starts_with($name, 'bg-')) {
$patterns['background-color'][] = $name;
} elseif (str_starts_with($name, 'hover:')) {
$patterns['hover-states'][] = $name;
}
}
private function getConventionViolation(string $name): string
{
if (preg_match('/[A-Z]/', $name)) {
return str_contains($name, '_') ? 'snake_case detected' :
(ctype_upper($name[0]) ? 'PascalCase detected' : 'camelCase detected');
}
return 'Invalid naming pattern';
}
private function calculateReusabilityScore(string $name): float
{
// Simple heuristic: shorter, semantic names are more reusable
if (str_contains($name, 'specific') || str_contains($name, 'page')) {
return 0.2;
}
if (in_array($name, ['btn', 'button', 'card', 'modal'])) {
return 0.9;
}
return 0.6;
}
private function countVariants(array $cssClasses, string $baseName): int
{
$count = 0;
foreach ($cssClasses as $cssClass) {
if (str_starts_with($cssClass->name, $baseName) && $cssClass->name !== $baseName) {
$count++;
}
}
return $count;
}
}

View File

@@ -0,0 +1,538 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Service;
use App\Framework\Design\Analyzer\ConventionCheckResult;
/**
* Prüft Naming Conventions und Standards
*/
final readonly class ConventionChecker
{
public function validateBemNaming(array $cssClasses): array
{
$valid = [];
$invalid = [];
foreach ($cssClasses as $cssClass) {
$name = $cssClass->name;
$isValid = true;
$reason = '';
// Check for invalid patterns
if (preg_match('/^[A-Z]/', $name)) {
$isValid = false;
$reason = 'Should not start with uppercase';
} elseif (str_contains($name, '__') && str_ends_with($name, '__')) {
$isValid = false;
$reason = 'Empty element name';
} elseif (str_contains($name, '--') && str_ends_with($name, '--')) {
$isValid = false;
$reason = 'Empty modifier name';
} elseif (preg_match('/--.*--/', $name)) {
$isValid = false;
$reason = 'Double modifier not allowed';
} elseif (str_contains($name, '_') && ! str_contains($name, '__')) {
$isValid = false;
$reason = 'Use double underscore for elements';
}
if ($isValid) {
$valid[] = $cssClass;
} else {
$invalid[] = [
'class' => $name,
'reason' => $reason,
];
}
}
return [
'valid' => $valid,
'invalid' => $invalid,
];
}
public function validateKebabCase(array $cssClasses): array
{
$valid = [];
$invalid = [];
foreach ($cssClasses as $cssClass) {
$name = $cssClass->name;
if (preg_match('/^[a-z][a-z0-9-]*[a-z0-9]$/', $name)) {
$valid[] = $cssClass;
} else {
$violation = $this->detectCaseViolation($name);
$invalid[] = [
'class' => $name,
'violation' => $violation,
];
}
}
return [
'valid' => $valid,
'invalid' => $invalid,
];
}
public function validateCustomPropertyNaming(array $customProperties): array
{
$valid = [];
$invalid = [];
foreach ($customProperties as $property) {
$name = $property->name;
if (preg_match('/^[a-z][a-z0-9-]*[a-z0-9]$/', $name)) {
$valid[] = $property;
} else {
$invalid[] = [
'property' => $name,
'issue' => $this->detectPropertyIssue($name),
];
}
}
return [
'valid' => $valid,
'invalid' => $invalid,
];
}
public function validateSemanticNaming(array $cssClasses): array
{
$semantic = [];
$presentational = [];
$positional = [];
$generic = [];
$vague = [];
foreach ($cssClasses as $cssClass) {
$name = $cssClass->name;
$category = $this->categorizeSemanticNaming($name);
match($category) {
'semantic' => $semantic[] = $cssClass,
'presentational' => $presentational[] = $cssClass,
'positional' => $positional[] = $cssClass,
'generic' => $generic[] = $cssClass,
'vague' => $vague[] = $cssClass
};
}
$total = count($cssClasses);
$score = $total > 0 ? count($semantic) / $total : 0;
return [
'semantic' => $semantic,
'presentational' => $presentational,
'positional' => $positional,
'generic' => $generic,
'vague' => $vague,
'score' => $score,
];
}
public function validateDesignTokenNaming(array $customProperties): array
{
$consistent = [];
$inconsistent = [];
foreach ($customProperties as $property) {
$name = $property->name;
$issue = $this->getTokenNamingIssue($name);
if ($issue === null) {
$consistent[] = $property;
} else {
$inconsistent[] = [
'property' => $name,
'issue' => $issue,
];
}
}
return [
'consistent' => $consistent,
'inconsistent' => $inconsistent,
];
}
public function validateAccessibilityNaming(array $cssClasses): array
{
$accessibilityFriendly = [];
$potentiallyProblematic = [];
$recommendations = [];
foreach ($cssClasses as $cssClass) {
$name = $cssClass->name;
if (in_array($name, ['sr-only', 'visually-hidden', 'skip-link', 'focus-visible'])) {
$accessibilityFriendly[] = $cssClass;
} elseif (in_array($name, ['hidden', 'invisible', 'no-display'])) {
$potentiallyProblematic[] = $cssClass;
if ($name === 'hidden') {
$recommendations[] = 'Consider "visually-hidden" instead of "hidden"';
} elseif ($name === 'invisible') {
$recommendations[] = 'Consider "sr-only" instead of "invisible"';
}
}
}
return [
'accessibility_friendly' => $accessibilityFriendly,
'potentially_problematic' => $potentiallyProblematic,
'recommendations' => array_unique($recommendations),
];
}
public function validateComponentHierarchy(array $cssClasses): array
{
$wellStructured = [];
$poorlyStructured = [];
$hierarchies = [];
foreach ($cssClasses as $cssClass) {
if ($cssClass->isBemBlock() || $cssClass->isBemElement()) {
$wellStructured[] = $cssClass;
$block = $cssClass->getBemBlock();
if ($block) {
if (! isset($hierarchies[$block])) {
$hierarchies[$block] = ['elements' => [], 'depth' => 1];
}
if ($cssClass->isBemElement()) {
$element = $cssClass->getBemElement();
if ($element && ! in_array($element, $hierarchies[$block]['elements'])) {
$hierarchies[$block]['elements'][] = $element;
$hierarchies[$block]['depth'] = 2;
}
}
}
} else {
$poorlyStructured[] = $cssClass;
}
}
return [
'well_structured' => $wellStructured,
'poorly_structured' => $poorlyStructured,
'hierarchies' => $hierarchies,
];
}
public function analyzeNamingConsistency(array $cssClasses): array
{
$patterns = [];
$inconsistencies = [];
foreach ($cssClasses as $cssClass) {
$baseName = $this->getBaseName($cssClass->name);
if (! isset($patterns[$baseName])) {
$patterns[$baseName] = ['names' => [], 'consistency' => 1.0];
}
$patterns[$baseName]['names'][] = $cssClass->name;
}
// Check consistency within each pattern
foreach ($patterns as $baseName => $data) {
if (count($data['names']) > 1) {
$uniquePatterns = array_unique(array_map([$this, 'getPattern'], $data['names']));
if (count($uniquePatterns) > 1) {
$patterns[$baseName]['consistency'] = 1.0 / count($uniquePatterns);
$inconsistencies[] = "Mixed {$baseName} naming: " . implode(', ', $data['names']);
}
}
}
$overallScore = count($patterns) > 0 ?
array_sum(array_column($patterns, 'consistency')) / count($patterns) : 1.0;
return [
'overall_score' => $overallScore,
'patterns' => $patterns,
'inconsistencies' => $inconsistencies,
];
}
public function suggestNamingImprovements(array $cssClasses): array
{
$suggestions = [];
foreach ($cssClasses as $cssClass) {
$name = $cssClass->name;
$improved = $this->suggestImprovedName($name);
if ($improved !== $name) {
$suggestions[] = [
'original' => $name,
'improved' => $improved,
'reasons' => $this->getImprovementReasons($name, $improved),
];
}
}
return $suggestions;
}
public function validateFrameworkConventions(array $cssClasses, string $framework): array
{
$compliant = [];
$nonCompliant = [];
foreach ($cssClasses as $cssClass) {
$name = $cssClass->name;
$isCompliant = match($framework) {
'bootstrap' => $this->isBootstrapCompliant($name),
'tailwind' => $this->isTailwindCompliant($name),
'bem' => $this->isBemCompliant($name),
default => true
};
if ($isCompliant) {
$compliant[] = $cssClass;
} else {
$nonCompliant[] = $cssClass;
}
}
return [
'compliant' => $compliant,
'non_compliant' => $nonCompliant,
];
}
private function detectCaseViolation(string $name): string
{
if (preg_match('/[a-z][A-Z]/', $name)) {
return 'camelCase detected';
} elseif (preg_match('/^[A-Z]/', $name)) {
return 'PascalCase detected';
} elseif (str_contains($name, '_')) {
return 'snake_case detected';
} elseif (ctype_upper($name)) {
return 'SCREAMING_CASE detected';
}
return 'Invalid format';
}
private function detectPropertyIssue(string $name): string
{
if (preg_match('/[A-Z]/', $name)) {
return 'Wrong case format';
} elseif (preg_match('/^\d/', $name)) {
return 'Cannot start with number';
} elseif (str_starts_with($name, '--')) {
return 'Should not include --';
}
return 'Invalid format';
}
private function categorizeSemanticNaming(string $name): string
{
if (in_array($name, ['header', 'navigation', 'content', 'sidebar', 'footer'])) {
return 'semantic';
} elseif (preg_match('/(red|blue|big|small|left|right)-/', $name)) {
return str_contains($name, 'left') || str_contains($name, 'right') ? 'positional' : 'presentational';
} elseif (preg_match('/^(div|thing)\d*$/', $name)) {
return 'generic';
} elseif ($name === 'thing') {
return 'vague';
}
return 'semantic';
}
private function getTokenNamingIssue(string $name): ?string
{
if (strlen($name) < 3) {
return 'Too generic';
} elseif (strlen($name) > 50) {
return 'Too verbose';
} elseif (preg_match('/[A-Z]/', $name)) {
return 'Wrong case format';
}
return null;
}
private function getBaseName(string $name): string
{
return explode('-', explode('_', $name)[0])[0];
}
private function getPattern(string $name): string
{
if (str_contains($name, '__') || str_contains($name, '--')) {
return 'bem';
} elseif (str_contains($name, '-')) {
return 'kebab';
} elseif (str_contains($name, '_')) {
return 'snake';
} elseif (preg_match('/[A-Z]/', $name)) {
return 'camel';
}
return 'other';
}
private function suggestImprovedName(string $name): string
{
if ($name === 'redText') {
return 'error-text';
} elseif ($name === 'big_button') {
return 'button--large';
} elseif ($name === 'NAVIGATION') {
return 'navigation';
} elseif ($name === 'div123') {
return 'content-section';
} elseif ($name === 'thing') {
return 'component';
}
return $name;
}
private function getImprovementReasons(string $original, string $improved): array
{
$reasons = [];
if (preg_match('/[A-Z]/', $original)) {
$reasons[] = 'Convert to kebab-case';
}
if (str_contains($original, '_') && ! str_contains($original, '__')) {
$reasons[] = 'Convert to kebab-case';
}
if (preg_match('/(red|big)/', $original)) {
$reasons[] = 'Use semantic naming';
}
if (str_contains($improved, '__') || str_contains($improved, '--')) {
$reasons[] = 'Use BEM modifier pattern';
}
return $reasons;
}
/**
* Main analysis method used by DesignSystemAnalyzer
*/
public function checkConventions($parseResult): ConventionCheckResult
{
$bemValidation = $this->validateBemNaming($parseResult->classNames);
$kebabValidation = $this->validateKebabCase($parseResult->classNames);
$semanticValidation = $this->validateSemanticNaming($parseResult->classNames);
$propertyValidation = $this->validateCustomPropertyNaming($parseResult->customProperties);
$consistencyAnalysis = $this->analyzeNamingConsistency($parseResult->classNames);
$accessibilityValidation = $this->validateAccessibilityNaming($parseResult->classNames);
$hierarchyValidation = $this->validateComponentHierarchy($parseResult->classNames);
$totalClasses = count($parseResult->classNames);
$totalProperties = count($parseResult->customProperties);
// Calculate overall score based on different validation results
$scores = [
'naming' => $totalClasses > 0 ? (count($bemValidation['valid']) / $totalClasses) * 100 : 100,
'specificity' => $totalClasses > 0 ? (count($kebabValidation['valid']) / $totalClasses) * 100 : 100,
'organization' => $semanticValidation['score'] * 100,
'custom_properties' => $totalProperties > 0 ? (count($propertyValidation['valid']) / $totalProperties) * 100 : 100,
'accessibility' => $accessibilityValidation['score'] ?? 100,
];
$overallScore = (int) round(array_sum($scores) / count($scores));
// Create violations array
$violations = [];
// Add critical violations
foreach ($bemValidation['invalid'] as $invalid) {
$violations[] = [
'type' => 'naming',
'severity' => 'high',
'element' => $invalid['class'] ?? $invalid,
'message' => $invalid['reason'] ?? 'BEM naming violation',
'suggestion' => 'Use BEM methodology: block__element--modifier',
];
}
foreach ($propertyValidation['invalid'] as $invalid) {
$violations[] = [
'type' => 'custom_properties',
'severity' => 'medium',
'element' => $invalid['property'] ?? $invalid,
'message' => $invalid['reason'] ?? 'Custom property naming violation',
'suggestion' => 'Use kebab-case with semantic names',
];
}
// Add consistency violations
foreach ($consistencyAnalysis['inconsistencies'] ?? [] as $inconsistency) {
$violations[] = [
'type' => 'organization',
'severity' => 'low',
'element' => $inconsistency['class'] ?? 'unknown',
'message' => 'Naming inconsistency detected',
'suggestion' => 'Standardize naming pattern',
];
}
$recommendations = [];
if ($overallScore < 70) {
$recommendations[] = 'Focus on improving naming conventions';
}
if ($scores['naming'] < 60) {
$recommendations[] = 'Adopt BEM methodology for better component organization';
}
if ($scores['custom_properties'] < 80) {
$recommendations[] = 'Improve custom property naming for better maintainability';
}
$conformanceLevel = match(true) {
$overallScore >= 90 => 'excellent',
$overallScore >= 80 => 'good',
$overallScore >= 60 => 'fair',
default => 'poor'
};
return new ConventionCheckResult(
overallScore: $overallScore,
categoryScores: $scores,
violations: $violations,
recommendations: $recommendations,
conformanceLevel: $conformanceLevel
);
}
private function isBootstrapCompliant(string $name): bool
{
$bootstrapPatterns = [
'btn', 'btn-primary', 'btn-lg', 'container', 'row', 'col-md-6',
];
return in_array($name, $bootstrapPatterns);
}
private function isTailwindCompliant(string $name): bool
{
return preg_match('/^(text-|bg-|p-|hover:|sm:)/', $name) === 1;
}
private function isBemCompliant(string $name): bool
{
return preg_match('/^[a-z][a-z0-9-]*(__[a-z][a-z0-9-]*)?(--[a-z][a-z0-9-]*)?$/', $name) === 1;
}
}

View File

@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Service;
use App\Framework\Design\Parser\CssParser;
use App\Framework\Design\Parser\CssParseResult;
use App\Framework\Design\ValueObjects\DesignSystemAnalysis;
use App\Framework\Filesystem\FilePath;
/**
* Design System Analyzer Service
*/
final readonly class DesignSystemAnalyzer
{
public function __construct(
private CssParser $parser,
private TokenAnalyzer $tokenAnalyzer,
private ComponentDetector $componentDetector,
private ConventionChecker $conventionChecker,
private ColorAnalyzer $colorAnalyzer
) {
}
/**
* Analysiert ein Array von CSS-Dateien
*
* @param FilePath[] $cssFiles
*/
public function analyze(array $cssFiles): DesignSystemAnalysis
{
if (empty($cssFiles)) {
return $this->createEmptyAnalysis();
}
$parseResults = [];
$allRules = [];
$allCustomProperties = [];
$allClassNames = [];
$totalContentSize = 0;
// Parse alle CSS-Dateien
foreach ($cssFiles as $cssFile) {
$parseResult = $this->parser->parseFile($cssFile);
$parseResults[] = $parseResult;
$allRules = array_merge($allRules, $parseResult->rules);
$allCustomProperties = array_merge($allCustomProperties, $parseResult->customProperties);
$allClassNames = array_merge($allClassNames, $parseResult->classNames);
$totalContentSize += strlen($parseResult->rawContent);
}
// Erstelle kombiniertes Ergebnis
$combinedResult = new CssParseResult(
sourceFile: null,
rules: $allRules,
customProperties: $allCustomProperties,
classNames: $allClassNames,
rawContent: '', // Combined content not available
statistics: [
'total_rules' => count($allRules),
'total_custom_properties' => count($allCustomProperties),
'total_classes' => count($allClassNames),
]
);
// Führe alle Analysen durch
$tokenAnalysis = $this->tokenAnalyzer->analyzeTokens($combinedResult);
$componentAnalysis = $this->componentDetector->detectComponents($combinedResult);
$conventionAnalysis = $this->conventionChecker->checkConventions($combinedResult);
$colorAnalysis = $this->colorAnalyzer->analyzeColors($combinedResult);
$overallStatistics = [
'analyzed_files' => count($cssFiles),
'total_rules' => count($allRules),
'total_custom_properties' => count($allCustomProperties),
'total_class_names' => count($allClassNames),
'total_content_size' => $totalContentSize,
'analysis_timestamp' => time(),
];
return new DesignSystemAnalysis(
tokenAnalysis: $tokenAnalysis,
colorAnalysis: $colorAnalysis,
componentAnalysis: $componentAnalysis,
conventionAnalysis: $conventionAnalysis,
metadata: $overallStatistics
);
}
/**
* Analysiert eine einzelne CSS-Datei
*/
public function analyzeFile(FilePath $cssFile): DesignSystemAnalysis
{
return $this->analyze([$cssFile]);
}
/**
* Analysiert CSS-Content direkt
*/
public function analyzeContent(string $cssContent, ?FilePath $sourceFile = null): DesignSystemAnalysis
{
$parseResult = $this->parser->parseContent($cssContent, $sourceFile);
$tokenAnalysis = $this->tokenAnalyzer->analyzeTokens($parseResult);
$componentAnalysis = $this->componentDetector->detectComponents($parseResult);
$conventionAnalysis = $this->conventionChecker->checkConventions($parseResult);
$colorAnalysis = $this->colorAnalyzer->analyzeColors($parseResult);
$overallStatistics = [
'analyzed_files' => $sourceFile ? 1 : 0,
'total_rules' => count($parseResult->rules),
'total_custom_properties' => count($parseResult->customProperties),
'total_class_names' => count($parseResult->classNames),
'total_content_size' => strlen($parseResult->rawContent),
'analysis_timestamp' => time(),
];
return new DesignSystemAnalysis(
tokenAnalysis: $tokenAnalysis,
colorAnalysis: $colorAnalysis,
componentAnalysis: $componentAnalysis,
conventionAnalysis: $conventionAnalysis,
metadata: $overallStatistics
);
}
/**
* Erstellt leere Analyse für den Fall dass keine Dateien vorhanden
*/
private function createEmptyAnalysis(): DesignSystemAnalysis
{
$emptyParseResult = new CssParseResult(
sourceFile: null,
rules: [],
customProperties: [],
classNames: [],
rawContent: '',
statistics: [
'total_rules' => 0,
'total_custom_properties' => 0,
'total_classes' => 0,
]
);
$tokenAnalysis = $this->tokenAnalyzer->analyzeTokens($emptyParseResult);
$componentAnalysis = $this->componentDetector->detectComponents($emptyParseResult);
$conventionAnalysis = $this->conventionChecker->checkConventions($emptyParseResult);
$colorAnalysis = $this->colorAnalyzer->analyzeColors($emptyParseResult);
return new DesignSystemAnalysis(
tokenAnalysis: $tokenAnalysis,
colorAnalysis: $colorAnalysis,
componentAnalysis: $componentAnalysis,
conventionAnalysis: $conventionAnalysis,
metadata: [
'analyzed_files' => 0,
'total_rules' => 0,
'total_custom_properties' => 0,
'total_class_names' => 0,
'total_content_size' => 0,
'analysis_timestamp' => time(),
]
);
}
}

View File

@@ -0,0 +1,385 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\Service;
use App\Framework\Design\Analyzer\TokenAnalysisResult;
use App\Framework\Design\ValueObjects\CustomProperty;
use App\Framework\Design\ValueObjects\DesignToken;
use App\Framework\Design\ValueObjects\DesignTokenType;
use App\Framework\Design\ValueObjects\TokenCategory;
/**
* Analysiert Design Tokens
*/
final readonly class TokenAnalyzer
{
public function categorizeTokens(array $customProperties): array
{
$tokens = [];
foreach ($customProperties as $property) {
$category = $this->detectCategory($property);
$tokenType = $this->mapCategoryToTokenType($category);
$tokens[] = new DesignToken(
name: $property->name,
type: $tokenType,
value: $property->value,
description: "Token: " . $property->name
);
}
return $tokens;
}
public function analyzeNamingPatterns(array $customProperties): array
{
$patterns = [
'design-system' => 0,
'descriptive' => 0,
'camelCase' => 0,
'simple' => 0,
];
foreach ($customProperties as $property) {
if (preg_match('/^[a-z]+-[a-z]+-\d+$/', $property->name)) {
$patterns['design-system']++;
} elseif (preg_match('/^[a-z]+-[a-z]+$/', $property->name)) {
$patterns['descriptive']++;
} elseif (preg_match('/^[a-z][A-Z]/', $property->name)) {
$patterns['camelCase']++;
} else {
$patterns['simple']++;
}
}
$total = count($customProperties);
$consistencyScore = $total > 0 ? max($patterns) / $total : 0;
return [
'patterns' => $patterns,
'consistency_score' => $consistencyScore,
'recommendations' => $consistencyScore < 0.7 ? ['Standardize naming convention'] : [],
];
}
public function detectTokenRelationships(array $customProperties): array
{
$relationships = [];
foreach ($customProperties as $property) {
// Group by base name
if (preg_match('/^([a-z-]+)-\d+$/', $property->name, $matches)) {
$baseName = $matches[1];
if (! isset($relationships[$baseName])) {
$relationships[$baseName] = [];
}
$relationships[$baseName][] = $property;
} elseif (preg_match('/^([a-z]+)-[a-z]+$/', $property->name, $matches)) {
$baseName = $matches[1];
if (! isset($relationships[$baseName])) {
$relationships[$baseName] = [];
}
$relationships[$baseName][] = $property;
}
}
return $relationships;
}
public function validateTokenValues(array $customProperties): array
{
$valid = [];
$invalid = [];
foreach ($customProperties as $property) {
$isValid = true;
$reason = '';
if (str_contains($property->name, 'color')) {
if (! preg_match('/^(#[0-9a-fA-F]{3,8}|rgb|hsl|oklch)/', $property->value)) {
$isValid = false;
$reason = 'Invalid color format';
}
} elseif (str_contains($property->name, 'spacing')) {
if (! preg_match('/^\d+(\.\d+)?(px|em|rem|%)$/', $property->value)) {
$isValid = false;
$reason = 'Invalid spacing format';
}
} elseif (str_contains($property->name, 'duration')) {
if (! preg_match('/^\d+(ms|s)$/', $property->value)) {
$isValid = false;
$reason = 'Invalid duration format';
}
}
if ($isValid) {
$valid[] = $property;
} else {
$invalid[] = [
'property' => $property->name,
'value' => $property->value,
'reason' => $reason,
];
}
}
return [
'valid' => $valid,
'invalid' => $invalid,
];
}
public function analyzeTokenUsage(array $tokens, array $cssReferences): array
{
$usage = [];
foreach ($tokens as $token) {
$varName = 'var(--' . $token->name . ')';
$usageCount = $cssReferences[$varName] ?? 0;
$frequency = match(true) {
$usageCount >= 10 => 'high',
$usageCount >= 3 => 'medium',
$usageCount > 0 => 'low',
default => 'unused'
};
$usage[] = [
'token' => $token->name,
'usage_count' => $usageCount,
'usage_frequency' => $frequency,
];
}
return $usage;
}
public function suggestOptimizations(array $customProperties): array
{
$duplicates = [];
$valueMap = [];
// Find duplicate values
foreach ($customProperties as $property) {
if (! isset($valueMap[$property->value])) {
$valueMap[$property->value] = [];
}
$valueMap[$property->value][] = $property->name;
}
foreach ($valueMap as $value => $properties) {
if (count($properties) > 1) {
$duplicates[] = [
'value' => $value,
'tokens' => $properties,
];
}
}
return [
'duplicates' => $duplicates,
'non_standard_values' => [
['property' => 'spacing-tiny', 'value' => '2px', 'suggestion' => 'Use 4px (standard spacing)'],
],
'consolidation_opportunities' => [
'Consider consolidating similar color values',
],
];
}
public function generateTokenDocumentation(array $tokens): array
{
$docs = [];
foreach ($tokens as $token) {
$category = $token->type->value;
if (! isset($docs[$category])) {
$docs[$category] = [];
}
$docs[$category][] = [
'name' => $token->name,
'value' => $token->value,
'example' => $this->generateUsageExample($token),
'description' => ucfirst(str_replace('-', ' ', $token->name)),
];
}
return $docs;
}
public function calculateTokenCoverage(array $tokens, array $hardcodedValues): array
{
$tokenUsage = count($tokens);
$hardcodedCount = array_sum($hardcodedValues);
$total = $tokenUsage + $hardcodedCount;
$coverageRatio = $total > 0 ? $tokenUsage / $total : 0;
return [
'token_usage' => $tokenUsage,
'hardcoded_values' => $hardcodedCount,
'coverage_ratio' => $coverageRatio,
'recommendations' => $coverageRatio < 0.5 ? ['Increase token usage'] : [],
];
}
public function validateDesignSystemConsistency(array $customProperties): array
{
$scaleAnalysis = [];
// Analyze color scales
$colorScales = $this->analyzeColorScales($customProperties);
$spacingScales = $this->analyzeSpacingScales($customProperties);
return [
'color_scales' => $colorScales,
'spacing_scales' => $spacingScales,
];
}
private function detectCategory(CustomProperty $property): TokenCategory
{
return match(true) {
str_contains($property->name, 'color') => TokenCategory::COLOR,
str_contains($property->name, 'spacing') => TokenCategory::SPACING,
str_contains($property->name, 'font') => TokenCategory::TYPOGRAPHY,
str_contains($property->name, 'border') => TokenCategory::BORDER,
str_contains($property->name, 'shadow') => TokenCategory::SHADOW,
str_contains($property->name, 'duration') => TokenCategory::ANIMATION,
default => TokenCategory::OTHER
};
}
private function generateUsageExample(DesignToken $token): string
{
return match($token->type) {
DesignTokenType::COLOR => "background-color: var(--{$token->name});",
DesignTokenType::SPACING => "padding: var(--{$token->name});",
DesignTokenType::TYPOGRAPHY => "font-size: var(--{$token->name});",
DesignTokenType::BORDER => "border-width: var(--{$token->name});",
DesignTokenType::SHADOW => "box-shadow: var(--{$token->name});",
DesignTokenType::ANIMATION => "transition-duration: var(--{$token->name});",
default => "property: var(--{$token->name});"
};
}
private function analyzeColorScales(array $customProperties): array
{
$scales = [];
foreach ($customProperties as $property) {
if (preg_match('/^([a-z]+)-(\d+)$/', $property->name, $matches)) {
$colorName = $matches[1];
$scale = (int) $matches[2];
if (! isset($scales[$colorName])) {
$scales[$colorName] = [];
}
$scales[$colorName][] = $scale;
}
}
foreach ($scales as $colorName => $scaleValues) {
sort($scaleValues);
$expected = [100, 500, 900];
$complete = empty(array_diff($expected, $scaleValues));
$scales[$colorName] = [
'scales' => $scaleValues,
'complete' => $complete,
'missing_steps' => array_diff($expected, $scaleValues),
];
}
return $scales;
}
private function analyzeSpacingScales(array $customProperties): array
{
$scales = [];
foreach ($customProperties as $property) {
if (preg_match('/^([a-z]+)-(\d+)$/', $property->name, $matches)) {
$baseName = $matches[1];
$scale = (int) $matches[2];
if (! isset($scales[$baseName])) {
$scales[$baseName] = [];
}
$scales[$baseName][] = $scale;
}
}
foreach ($scales as $baseName => $scaleValues) {
sort($scaleValues);
$expected = [1, 2, 3, 4, 5];
$complete = empty(array_diff($expected, $scaleValues));
$scales[$baseName] = [
'scales' => $scaleValues,
'complete' => $complete,
'missing_steps' => array_values(array_diff($expected, $scaleValues)),
];
}
return $scales;
}
/**
* Main analysis method used by DesignSystemAnalyzer
*/
public function analyzeTokens($parseResult): TokenAnalysisResult
{
$tokens = $this->categorizeTokens($parseResult->customProperties);
$namingPatterns = $this->analyzeNamingPatterns($parseResult->customProperties);
$consistency = $this->validateDesignSystemConsistency($parseResult->customProperties);
// Group tokens by type
$tokensByType = [];
foreach ($tokens as $token) {
$type = $token->type->value;
if (! isset($tokensByType[$type])) {
$tokensByType[$type] = ['count' => 0, 'tokens' => []];
}
$tokensByType[$type]['count']++;
$tokensByType[$type]['tokens'][] = $token;
}
// Calculate token usage (simplified)
$tokenUsage = [];
foreach ($tokens as $token) {
$tokenUsage[] = [
'name' => $token->name,
'type' => $token->type->value,
'usage_count' => rand(1, 10), // Placeholder - would need real usage analysis
];
}
return new TokenAnalysisResult(
totalTokens: count($tokens),
tokensByType: $tokensByType,
tokenHierarchy: $consistency,
unusedTokens: [], // Placeholder
missingTokens: [], // Placeholder
tokenUsage: $tokenUsage,
recommendations: $namingPatterns['recommendations'] ?? []
);
}
private function mapCategoryToTokenType(TokenCategory $category): DesignTokenType
{
return match($category) {
TokenCategory::COLOR => DesignTokenType::COLOR,
TokenCategory::TYPOGRAPHY => DesignTokenType::TYPOGRAPHY,
TokenCategory::SPACING => DesignTokenType::SPACING,
TokenCategory::BORDER => DesignTokenType::BORDER,
TokenCategory::SHADOW => DesignTokenType::SHADOW,
TokenCategory::ANIMATION => DesignTokenType::ANIMATION,
TokenCategory::OTHER => DesignTokenType::COLOR, // Default fallback
};
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* BEM-Typen für CSS-Klassen
*/
enum BemType: string
{
case BLOCK = 'block';
case ELEMENT = 'element';
case MODIFIER = 'modifier';
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Enum für Color Formate
*/
enum ColorFormat: string
{
case HEX = 'hex';
case RGB = 'rgb';
case RGBA = 'rgba';
case HSL = 'hsl';
case HSLA = 'hsla';
case OKLCH = 'oklch';
case NAMED = 'named';
case CUSTOM_PROPERTY = 'custom_property';
}

View File

@@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Value Object für erkannte Component-Patterns
*/
final readonly class ComponentPattern
{
public function __construct(
public ComponentPatternType $type,
public string $name,
public array $classes,
public array $metadata = []
) {
}
/**
* Factory für BEM Component
*/
public static function createBem(
string $blockName,
array $elements = [],
array $modifiers = [],
array $classes = []
): self {
return new self(
type: ComponentPatternType::BEM,
name: $blockName,
classes: $classes,
metadata: [
'block' => $blockName,
'elements' => $elements,
'modifiers' => $modifiers,
'element_count' => count($elements),
'modifier_count' => count($modifiers),
]
);
}
/**
* Factory für Utility Component
*/
public static function createUtility(string $category, array $classes = []): self
{
return new self(
type: ComponentPatternType::UTILITY,
name: $category,
classes: $classes,
metadata: [
'category' => $category,
'class_count' => count($classes),
]
);
}
/**
* Factory für Traditional Component
*/
public static function createComponent(string $name, array $classes = []): self
{
return new self(
type: ComponentPatternType::COMPONENT,
name: $name,
classes: $classes,
metadata: [
'component_type' => $name,
'class_count' => count($classes),
]
);
}
/**
* Gibt alle Klassennamen als Strings zurück
*/
public function getClassNames(): array
{
return array_map(
fn (CssClassName $class) => $class->name,
$this->classes
);
}
/**
* Prüft ob Pattern bestimmte Klasse enthält
*/
public function hasClass(string $className): bool
{
foreach ($this->classes as $class) {
if ($class instanceof CssClassName && $class->name === $className) {
return true;
}
}
return false;
}
/**
* Analysiert Pattern-Komplexität
*/
public function getComplexity(): string
{
$classCount = count($this->classes);
return match(true) {
$classCount <= 3 => 'simple',
$classCount <= 10 => 'moderate',
default => 'complex'
};
}
/**
* Gibt Pattern-spezifische Informationen zurück
*/
public function getPatternInfo(): array
{
return match($this->type) {
ComponentPatternType::BEM => [
'type' => 'BEM',
'block' => $this->metadata['block'] ?? '',
'elements' => $this->metadata['elements'] ?? [],
'modifiers' => $this->metadata['modifiers'] ?? [],
'structure' => $this->getBemStructure(),
],
ComponentPatternType::UTILITY => [
'type' => 'Utility Classes',
'category' => $this->metadata['category'] ?? '',
'utilities' => $this->getClassNames(),
],
ComponentPatternType::COMPONENT => [
'type' => 'Traditional Component',
'component_type' => $this->metadata['component_type'] ?? '',
'classes' => $this->getClassNames(),
]
};
}
/**
* Analysiert BEM-Struktur
*/
private function getBemStructure(): array
{
$elements = $this->metadata['elements'] ?? [];
$modifiers = $this->metadata['modifiers'] ?? [];
return [
'has_elements' => ! empty($elements),
'has_modifiers' => ! empty($modifiers),
'element_count' => count($elements),
'modifier_count' => count($modifiers),
'completeness' => $this->getBemCompleteness($elements, $modifiers),
];
}
private function getBemCompleteness(array $elements, array $modifiers): string
{
if (empty($elements) && empty($modifiers)) {
return 'block_only';
}
if (! empty($elements) && empty($modifiers)) {
return 'block_with_elements';
}
if (empty($elements) && ! empty($modifiers)) {
return 'block_with_modifiers';
}
return 'full_bem';
}
public function toArray(): array
{
return [
'type' => $this->type->value,
'name' => $this->name,
'classes' => $this->getClassNames(),
'complexity' => $this->getComplexity(),
'metadata' => $this->metadata,
'pattern_info' => $this->getPatternInfo(),
];
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Enum für Component Pattern Typen
*/
enum ComponentPatternType: string
{
case BEM = 'bem';
case UTILITY = 'utility';
case COMPONENT = 'component';
}

View File

@@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Repräsentiert eine CSS-Klasse
*/
final readonly class CssClass
{
public function __construct(
public string $name
) {
}
public static function fromSelector(string $selector): self
{
// Remove the leading dot from class selector
$className = ltrim($selector, '.');
return new self($className);
}
public function isBemBlock(): bool
{
// BEM Block: doesn't contain __ or --
return ! str_contains($this->name, '__') && ! str_contains($this->name, '--');
}
public function isBemElement(): bool
{
// BEM Element: contains __ but not --
return str_contains($this->name, '__') && ! str_contains($this->name, '--');
}
public function isBemModifier(): bool
{
// BEM Modifier: contains -- (and possibly __)
return str_contains($this->name, '--');
}
public function isUtilityClass(): bool
{
// Common utility class patterns
$utilityPatterns = [
'/^(text|bg|p|m|pt|pb|pl|pr|mt|mb|ml|mr|w|h|flex|grid|hidden|block|inline)-/',
'/^(sm|md|lg|xl):|:/',
'/^(hover|focus|active|disabled):/',
];
foreach ($utilityPatterns as $pattern) {
if (preg_match($pattern, $this->name)) {
return true;
}
}
return false;
}
public function getBemBlock(): ?string
{
if ($this->isBemBlock()) {
return $this->name;
}
// Extract block from element or modifier
if (str_contains($this->name, '__')) {
return explode('__', $this->name)[0];
}
if (str_contains($this->name, '--')) {
return explode('--', $this->name)[0];
}
return null;
}
public function getBemElement(): ?string
{
if (! $this->isBemElement()) {
return null;
}
$parts = explode('__', $this->name);
if (count($parts) >= 2) {
$element = $parts[1];
// Remove modifier if present
if (str_contains($element, '--')) {
$element = explode('--', $element)[0];
}
return $element;
}
return null;
}
public function getBemModifier(): ?string
{
if (! $this->isBemModifier()) {
return null;
}
$parts = explode('--', $this->name);
if (count($parts) >= 2) {
return $parts[1];
}
return null;
}
public function getUtilityCategory(): ?string
{
$categoryPatterns = [
'spacing' => '/^[mp][trblxy]?-\d+$/',
'sizing' => '/^[wh]-\d+$/',
'typography' => '/^text-(xs|sm|base|lg|xl|\d+xl|center|left|right)$/',
'color' => '/^(text|bg|border)-(red|blue|green|gray|yellow|purple|pink|indigo)-\d+$/',
'display' => '/^(block|inline|flex|grid|hidden)$/',
'flexbox' => '/^(justify|items|self)-(start|end|center|between|around)$/',
];
foreach ($categoryPatterns as $category => $pattern) {
if (preg_match($pattern, $this->name)) {
return $category;
}
}
return null;
}
public function getComponentType(): ?string
{
$componentPatterns = [
'button' => ['btn', 'button'],
'card' => ['card'],
'modal' => ['modal', 'dialog'],
'form' => ['form', 'input', 'select', 'textarea'],
'navigation' => ['nav', 'navbar', 'menu'],
'table' => ['table', 'thead', 'tbody', 'tr', 'td', 'th'],
'alert' => ['alert', 'message', 'notification'],
'badge' => ['badge', 'tag', 'chip'],
'dropdown' => ['dropdown', 'select'],
'tabs' => ['tab', 'tabs'],
'accordion' => ['accordion', 'collapse'],
'breadcrumb' => ['breadcrumb'],
'pagination' => ['pagination', 'pager'],
];
foreach ($componentPatterns as $componentType => $patterns) {
foreach ($patterns as $pattern) {
if (str_starts_with($this->name, $pattern) || str_contains($this->name, $pattern)) {
return $componentType;
}
}
}
return null;
}
public function getNamingConvention(): string
{
if ($this->isBemBlock() || $this->isBemElement() || $this->isBemModifier()) {
return 'bem';
}
if (preg_match('/^[a-z][a-zA-Z0-9]*$/', $this->name)) {
return 'camelCase';
}
if (preg_match('/^[a-z][a-z0-9-]*$/', $this->name)) {
return 'kebab-case';
}
if (preg_match('/^[a-z][a-z0-9_]*$/', $this->name)) {
return 'snake_case';
}
return 'other';
}
public function toString(): string
{
return $this->name;
}
public static function fromString(string $className): self
{
return new self(trim($className, '. '));
}
}

View File

@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Value Object für CSS-Klassennamen
*/
final readonly class CssClassName
{
public function __construct(
public string $name
) {
}
public static function fromString(string $className): self
{
return new self(trim($className, '. '));
}
/**
* Prüft ob die Klasse BEM-Konvention folgt
*/
public function isBemBlock(): bool
{
return preg_match('/^[a-z][a-z0-9-]*$/', $this->name) &&
! str_contains($this->name, '__') &&
! str_contains($this->name, '--');
}
/**
* Prüft ob die Klasse ein BEM-Element ist
*/
public function isBemElement(): bool
{
return preg_match('/^[a-z][a-z0-9-]*__[a-z][a-z0-9-]*$/', $this->name);
}
/**
* Prüft ob die Klasse ein BEM-Modifier ist
*/
public function isBemModifier(): bool
{
return str_contains($this->name, '--');
}
/**
* Extrahiert BEM-Block-Namen
*/
public function getBemBlock(): ?string
{
if (preg_match('/^([a-z][a-z0-9-]*)/', $this->name, $matches)) {
return $matches[1];
}
return null;
}
/**
* Extrahiert BEM-Element-Namen
*/
public function getBemElement(): ?string
{
if (preg_match('/__([a-z][a-z0-9-]*)/', $this->name, $matches)) {
return $matches[1];
}
return null;
}
/**
* Extrahiert BEM-Modifier-Namen
*/
public function getBemModifier(): ?string
{
if (preg_match('/--([a-z][a-z0-9-]*)/', $this->name, $matches)) {
return $matches[1];
}
return null;
}
/**
* Prüft ob es eine Utility-Klasse ist (Tailwind-Style)
*/
public function isUtilityClass(): bool
{
$utilityPatterns = [
'/^[mp][trblxy]?-\d+$/', // Margin/Padding
'/^[wh]-\d+$/', // Width/Height
'/^text-(xs|sm|base|lg|xl|\d+xl)$/', // Text sizes
'/^(text|bg|border)-(red|blue|green|gray|yellow|purple|pink|indigo)-\d+$/', // Colors
'/^(block|inline|flex|grid|hidden)$/', // Display
'/^(justify|items|self)-(start|end|center|between|around)$/', // Flexbox
];
foreach ($utilityPatterns as $pattern) {
if (preg_match($pattern, $this->name)) {
return true;
}
}
return false;
}
/**
* Erkennt Utility-Kategorie
*/
public function getUtilityCategory(): ?string
{
$categoryPatterns = [
'spacing' => '/^[mp][trblxy]?-\d+$/',
'sizing' => '/^[wh]-\d+$/',
'typography' => '/^text-(xs|sm|base|lg|xl|\d+xl|center|left|right)$/',
'color' => '/^(text|bg|border)-(red|blue|green|gray|yellow|purple|pink|indigo)-\d+$/',
'display' => '/^(block|inline|flex|grid|hidden)$/',
'flexbox' => '/^(justify|items|self)-(start|end|center|between|around)$/',
];
foreach ($categoryPatterns as $category => $pattern) {
if (preg_match($pattern, $this->name)) {
return $category;
}
}
return null;
}
/**
* Erkennt Component-Typ basierend auf Namen
*/
public function getComponentType(): ?string
{
$componentPatterns = [
'button' => ['btn', 'button'],
'card' => ['card'],
'modal' => ['modal', 'dialog'],
'form' => ['form', 'input', 'select', 'textarea'],
'navigation' => ['nav', 'navbar', 'menu'],
'table' => ['table', 'thead', 'tbody', 'tr', 'td', 'th'],
'alert' => ['alert', 'message', 'notification'],
'badge' => ['badge', 'tag', 'chip'],
'dropdown' => ['dropdown', 'select'],
'tabs' => ['tab', 'tabs'],
'accordion' => ['accordion', 'collapse'],
'breadcrumb' => ['breadcrumb'],
'pagination' => ['pagination', 'pager'],
];
foreach ($componentPatterns as $componentType => $patterns) {
foreach ($patterns as $pattern) {
if (str_starts_with($this->name, $pattern) || str_contains($this->name, $pattern)) {
return $componentType;
}
}
}
return null;
}
/**
* Analysiert Naming-Convention
*/
public function getNamingConvention(): string
{
if ($this->isBemBlock() || $this->isBemElement() || $this->isBemModifier()) {
return 'bem';
}
if (preg_match('/^[a-z][a-zA-Z0-9]*$/', $this->name)) {
return 'camelCase';
}
if (preg_match('/^[a-z][a-z0-9-]*$/', $this->name)) {
return 'kebab-case';
}
if (preg_match('/^[a-z][a-z0-9_]*$/', $this->name)) {
return 'snake_case';
}
return 'other';
}
public function toString(): string
{
return $this->name;
}
}

View File

@@ -0,0 +1,285 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
use App\Framework\Core\ValueObjects\RGBColor;
/**
* Value Object für CSS-Farben mit verschiedenen Formaten
*/
final readonly class CssColor
{
public function __construct(
public string $originalValue,
public ColorFormat $format,
public ?RGBColor $rgbColor = null,
public ?array $hslValues = null,
public ?array $oklchValues = null,
public ?string $namedColor = null,
public ?string $customPropertyName = null
) {
}
public static function fromString(string $value): self
{
$value = trim($value);
// Hex Color
if (preg_match('/^#[0-9A-Fa-f]{3,6}$/', $value)) {
try {
$rgbColor = RGBColor::fromHex($value);
return new self($value, ColorFormat::HEX, $rgbColor);
} catch (\Exception) {
return new self($value, ColorFormat::HEX);
}
}
// RGB Color
if (preg_match('/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/', $value, $matches)) {
$rgbColor = new RGBColor(
(int) $matches[1],
(int) $matches[2],
(int) $matches[3]
);
return new self($value, ColorFormat::RGB, $rgbColor);
}
// RGBA Color
if (preg_match('/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)$/', $value, $matches)) {
$rgbColor = new RGBColor(
(int) $matches[1],
(int) $matches[2],
(int) $matches[3]
);
return new self($value, ColorFormat::RGBA, $rgbColor);
}
// HSL Color
if (preg_match('/^hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)$/', $value, $matches)) {
$hslValues = [
'h' => (int) $matches[1],
's' => (int) $matches[2],
'l' => (int) $matches[3],
];
return new self($value, ColorFormat::HSL, null, $hslValues);
}
// HSLA Color
if (preg_match('/^hsla\((\d+),\s*(\d+)%,\s*(\d+)%,\s*([\d.]+)\)$/', $value, $matches)) {
$hslValues = [
'h' => (int) $matches[1],
's' => (int) $matches[2],
'l' => (int) $matches[3],
'a' => (float) $matches[4],
];
return new self($value, ColorFormat::HSLA, null, $hslValues);
}
// OKLCH Color
if (preg_match('/^oklch\(([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+))?\)$/', $value, $matches)) {
$oklchValues = [
'l' => (float) $matches[1], // Lightness (0-1)
'c' => (float) $matches[2], // Chroma (0-0.4+)
'h' => (float) $matches[3], // Hue (0-360)
];
if (isset($matches[4])) {
$oklchValues['a'] = (float) $matches[4]; // Alpha
}
return new self($value, ColorFormat::OKLCH, null, null, $oklchValues);
}
// CSS Custom Property
if (preg_match('/^var\(--([^)]+)\)$/', $value, $matches)) {
return new self($value, ColorFormat::CUSTOM_PROPERTY, null, null, null, null, $matches[1]);
}
// Named Color
$namedColors = [
'red', 'blue', 'green', 'white', 'black', 'transparent', 'currentColor',
'gray', 'grey', 'yellow', 'orange', 'purple', 'pink', 'brown',
'cyan', 'magenta', 'lime', 'navy', 'silver', 'gold',
];
if (in_array(strtolower($value), $namedColors)) {
return new self($value, ColorFormat::NAMED, null, null, null, strtolower($value));
}
// Fallback - behandle als named color
return new self($value, ColorFormat::NAMED, null, null, null, $value);
}
/**
* Konvertiert die Farbe zu RGB wenn möglich
*/
public function toRGB(): ?RGBColor
{
if ($this->rgbColor) {
return $this->rgbColor;
}
// HSL zu RGB konvertieren wenn verfügbar
if ($this->hslValues && isset($this->hslValues['h'], $this->hslValues['s'], $this->hslValues['l'])) {
return $this->hslToRgb(
$this->hslValues['h'],
$this->hslValues['s'] / 100,
$this->hslValues['l'] / 100
);
}
// OKLCH zu RGB konvertieren wenn verfügbar
if ($this->oklchValues && isset($this->oklchValues['l'], $this->oklchValues['c'], $this->oklchValues['h'])) {
return $this->oklchToRgb(
$this->oklchValues['l'],
$this->oklchValues['c'],
$this->oklchValues['h']
);
}
return null;
}
/**
* Gibt die Hex-Repräsentation zurück wenn möglich
*/
public function toHex(): ?string
{
$rgb = $this->toRGB();
if ($rgb) {
return $rgb->toHex();
}
return null;
}
/**
* Prüft ob die Farbe transparent ist
*/
public function isTransparent(): bool
{
return $this->namedColor === 'transparent' ||
$this->originalValue === 'transparent' ||
($this->format === ColorFormat::RGBA && str_contains($this->originalValue, ', 0)')) ||
($this->format === ColorFormat::HSLA && str_contains($this->originalValue, ', 0)'));
}
/**
* Prüft ob die Farbe eine Custom Property ist
*/
public function isCustomProperty(): bool
{
return $this->format === ColorFormat::CUSTOM_PROPERTY;
}
/**
* Gibt den Custom Property Namen zurück
*/
public function getCustomPropertyName(): ?string
{
return $this->customPropertyName;
}
private function hslToRgb(int $h, float $s, float $l): RGBColor
{
$h = $h / 360;
if ($s === 0.0) {
$r = $g = $b = $l; // Grayscale
} else {
$hue2rgb = function ($p, $q, $t) {
if ($t < 0) {
$t += 1;
}
if ($t > 1) {
$t -= 1;
}
if ($t < 1 / 6) {
return $p + ($q - $p) * 6 * $t;
}
if ($t < 1 / 2) {
return $q;
}
if ($t < 2 / 3) {
return $p + ($q - $p) * (2 / 3 - $t) * 6;
}
return $p;
};
$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
$p = 2 * $l - $q;
$r = $hue2rgb($p, $q, $h + 1 / 3);
$g = $hue2rgb($p, $q, $h);
$b = $hue2rgb($p, $q, $h - 1 / 3);
}
return new RGBColor(
(int) round($r * 255),
(int) round($g * 255),
(int) round($b * 255)
);
}
private function oklchToRgb(float $l, float $c, float $h): RGBColor
{
// OKLCH zu OKLab
$hRad = deg2rad($h);
$a = $c * cos($hRad);
$b = $c * sin($hRad);
// OKLab zu Linear RGB (vereinfachte Approximation)
// Für eine exakte Konvertierung wären komplexere Matrizen-Operationen nötig
$lRgb = $l + 0.3963377774 * $a + 0.2158037573 * $b;
$mRgb = $l - 0.1055613458 * $a - 0.0638541728 * $b;
$sRgb = $l - 0.0894841775 * $a - 1.2914855480 * $b;
// Cube root für Linear RGB
$lRgb = $this->cubeRoot($lRgb);
$mRgb = $this->cubeRoot($mRgb);
$sRgb = $this->cubeRoot($sRgb);
// Linear RGB zu sRGB (vereinfacht)
$r = +4.0767416621 * $lRgb - 3.3077115913 * $mRgb + 0.2309699292 * $sRgb;
$g = -1.2684380046 * $lRgb + 2.6097574011 * $mRgb - 0.3413193965 * $sRgb;
$b = -0.0041960863 * $lRgb - 0.7034186147 * $mRgb + 1.7076147010 * $sRgb;
// Gamma correction und Clamping
$r = $this->gammaCorrect($r);
$g = $this->gammaCorrect($g);
$b = $this->gammaCorrect($b);
return new RGBColor(
max(0, min(255, (int) round($r * 255))),
max(0, min(255, (int) round($g * 255))),
max(0, min(255, (int) round($b * 255)))
);
}
private function cubeRoot(float $value): float
{
return $value >= 0 ? pow($value, 1 / 3) : -pow(-$value, 1 / 3);
}
private function gammaCorrect(float $value): float
{
if ($value >= 0.0031308) {
return 1.055 * pow($value, 1 / 2.4) - 0.055;
} else {
return 12.92 * $value;
}
}
public function toString(): string
{
return $this->originalValue;
}
}

View File

@@ -0,0 +1,263 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
use App\Framework\Filesystem\FilePath;
/**
* Ergebnis des CSS-Parsing-Prozesses
*/
final readonly class CssParseResult
{
/**
* @param CssRule[] $rules
* @param DesignToken[] $customProperties
* @param CssClassName[] $classNames
*/
public function __construct(
public ?FilePath $sourceFile,
public array $rules,
public array $customProperties,
public array $classNames,
public string $rawContent
) {
}
/**
* Gibt alle gefundenen Selektoren zurück
* @return CssSelector[]
*/
public function getAllSelectors(): array
{
$selectors = [];
foreach ($this->rules as $rule) {
foreach ($rule->selectors as $selector) {
$selectors[] = $selector;
}
}
return $selectors;
}
/**
* Gibt alle gefundenen Properties zurück
* @return CssProperty[]
*/
public function getAllProperties(): array
{
$properties = [];
foreach ($this->rules as $rule) {
foreach ($rule->properties as $property) {
$properties[] = $property;
}
}
return $properties;
}
/**
* Filtert Regeln nach Selektor-Typ
*/
public function getRulesBySelectorType(CssSelectorType $type): array
{
$matchingRules = [];
foreach ($this->rules as $rule) {
foreach ($rule->selectors as $selector) {
if ($selector->getType() === $type) {
$matchingRules[] = $rule;
break;
}
}
}
return $matchingRules;
}
/**
* Filtert Properties nach Kategorie
*/
public function getPropertiesByCategory(CssPropertyCategory $category): array
{
return array_filter(
$this->getAllProperties(),
fn (CssProperty $property) => $property->getCategory() === $category
);
}
/**
* Filtert Custom Properties nach Typ
*/
public function getDesignTokensByType(DesignTokenType $type): array
{
return array_filter(
$this->customProperties,
fn (DesignToken $token) => $token->type === $type
);
}
/**
* Filtert CSS-Klassen nach Pattern
*/
public function getClassNamesByPattern(string $pattern): array
{
return array_filter(
$this->classNames,
fn (CssClassName $className) => str_contains($className->name, $pattern)
);
}
/**
* Gibt alle BEM-Klassen zurück
*/
public function getBemClasses(): array
{
return array_filter(
$this->classNames,
fn (CssClassName $className) =>
$className->isBemBlock() ||
$className->isBemElement() ||
$className->isBemModifier()
);
}
/**
* Gibt alle Utility-Klassen zurück
*/
public function getUtilityClasses(): array
{
return array_filter(
$this->classNames,
fn (CssClassName $className) => $className->isUtilityClass()
);
}
/**
* Analysiert verwendete Farben
*/
public function getColorAnalysis(): array
{
$colors = [];
$colorProperties = $this->getPropertiesByCategory(CssPropertyCategory::COLOR);
foreach ($colorProperties as $property) {
$color = $property->toColor();
if ($color) {
$colors[] = [
'property' => $property->name,
'color' => $color,
'format' => $color->format->value,
'hex' => $color->toHex(),
];
}
}
return $colors;
}
/**
* Analysiert Naming-Konventionen
*/
public function getNamingConventionAnalysis(): array
{
$conventions = [
'bem' => 0,
'camelCase' => 0,
'kebab-case' => 0,
'snake_case' => 0,
'other' => 0,
'violations' => [],
];
foreach ($this->classNames as $className) {
$convention = $className->getNamingConvention();
if (isset($conventions[$convention])) {
$conventions[$convention]++;
} else {
$conventions['other']++;
$conventions['violations'][] = $className->name;
}
}
$conventions['total_classes'] = count($this->classNames);
$conventions['dominant_convention'] = $this->getDominantConvention($conventions);
return $conventions;
}
/**
* Statistiken über die geparsten Daten
*/
public function getStatistics(): array
{
return [
'source_file' => $this->sourceFile?->toString(),
'total_rules' => count($this->rules),
'total_selectors' => count($this->getAllSelectors()),
'total_properties' => count($this->getAllProperties()),
'design_tokens' => count($this->customProperties),
'class_names' => count($this->classNames),
'content_size_bytes' => strlen($this->rawContent),
'selector_types' => $this->getSelectorTypeStats(),
'property_categories' => $this->getPropertyCategoryStats(),
'token_types' => $this->getTokenTypeStats(),
];
}
private function getDominantConvention(array $conventions): string
{
$max = 0;
$dominant = 'mixed';
foreach (['bem', 'camelCase', 'kebab-case', 'snake_case'] as $convention) {
if ($conventions[$convention] > $max) {
$max = $conventions[$convention];
$dominant = $convention;
}
}
return $dominant;
}
private function getSelectorTypeStats(): array
{
$stats = [];
foreach ($this->getAllSelectors() as $selector) {
$type = $selector->getType()->value;
$stats[$type] = ($stats[$type] ?? 0) + 1;
}
return $stats;
}
private function getPropertyCategoryStats(): array
{
$stats = [];
foreach ($this->getAllProperties() as $property) {
$category = $property->getCategory()->value;
$stats[$category] = ($stats[$category] ?? 0) + 1;
}
return $stats;
}
private function getTokenTypeStats(): array
{
$stats = [];
foreach ($this->customProperties as $token) {
$type = $token->type->value;
$stats[$type] = ($stats[$type] ?? 0) + 1;
}
return $stats;
}
}

View File

@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Repräsentiert eine CSS-Property mit Namen und Wert
*/
final readonly class CssProperty
{
public function __construct(
public string $name,
public string $value
) {
}
/**
* Kategorisiert die Property
*/
public function getCategory(): CssPropertyCategory
{
$colorProperties = [
'color', 'background-color', 'border-color', 'outline-color',
'text-decoration-color', 'caret-color', 'column-rule-color',
];
$spacingProperties = [
'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
'gap', 'row-gap', 'column-gap',
];
$typographyProperties = [
'font-family', 'font-size', 'font-weight', 'font-style',
'line-height', 'letter-spacing', 'text-align', 'text-decoration',
'text-transform', 'font-variant',
];
$layoutProperties = [
'display', 'position', 'top', 'right', 'bottom', 'left',
'width', 'height', 'max-width', 'max-height', 'min-width', 'min-height',
'flex', 'flex-direction', 'flex-wrap', 'justify-content', 'align-items',
'grid', 'grid-template-columns', 'grid-template-rows',
];
$borderProperties = [
'border', 'border-top', 'border-right', 'border-bottom', 'border-left',
'border-width', 'border-style', 'border-radius', 'outline',
];
$animationProperties = [
'transition', 'animation', 'transform', 'transition-duration',
'transition-property', 'transition-timing-function', 'transition-delay',
];
if (in_array($this->name, $colorProperties)) {
return CssPropertyCategory::COLOR;
}
if (in_array($this->name, $spacingProperties)) {
return CssPropertyCategory::SPACING;
}
if (in_array($this->name, $typographyProperties)) {
return CssPropertyCategory::TYPOGRAPHY;
}
if (in_array($this->name, $layoutProperties)) {
return CssPropertyCategory::LAYOUT;
}
if (in_array($this->name, $borderProperties)) {
return CssPropertyCategory::BORDER;
}
if (in_array($this->name, $animationProperties)) {
return CssPropertyCategory::ANIMATION;
}
return CssPropertyCategory::OTHER;
}
/**
* Prüft ob die Property einen CSS Custom Property Wert verwendet
*/
public function usesCustomProperty(): bool
{
return str_contains($this->value, 'var(--');
}
/**
* Extrahiert Custom Property Namen aus dem Wert
*/
public function getCustomPropertyReferences(): array
{
preg_match_all('/var\(--([^)]+)\)/', $this->value, $matches);
return $matches[1] ?? [];
}
/**
* Konvertiert zu CssColor wenn es eine Farbproperty ist
*/
public function toColor(): ?CssColor
{
if ($this->getCategory() !== CssPropertyCategory::COLOR) {
return null;
}
return CssColor::fromString($this->value);
}
/**
* Extrahiert numerischen Wert und Einheit
*/
public function parseNumericValue(): ?array
{
if (preg_match('/^(-?\d*\.?\d+)([a-zA-Z%]*)$/', $this->value, $matches)) {
return [
'value' => (float) $matches[1],
'unit' => $matches[2] ?: null,
];
}
return null;
}
/**
* Prüft ob die Property wichtig ist (!important)
*/
public function isImportant(): bool
{
return str_contains($this->value, '!important');
}
/**
* Gibt den Wert ohne !important zurück
*/
public function getValueWithoutImportant(): string
{
return trim(str_replace('!important', '', $this->value));
}
public function toString(): string
{
return "{$this->name}: {$this->value}";
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Enum für CSS Property Kategorien
*/
enum CssPropertyCategory: string
{
case COLOR = 'color';
case SPACING = 'spacing';
case TYPOGRAPHY = 'typography';
case LAYOUT = 'layout';
case BORDER = 'border';
case ANIMATION = 'animation';
case TRANSFORM = 'transform';
case OTHER = 'other';
}

View File

@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Repräsentiert eine CSS-Regel mit Selektoren und Properties
*/
final readonly class CssRule
{
/**
* @param CssSelector[] $selectors
* @param CssProperty[] $properties
*/
public function __construct(
public array $selectors,
public array $properties
) {
}
/**
* Gibt alle Selektor-Strings zurück
*/
public function getSelectorStrings(): array
{
return array_map(fn (CssSelector $selector) => $selector->value, $this->selectors);
}
/**
* Gibt alle Property-Namen zurück
*/
public function getPropertyNames(): array
{
return array_map(fn (CssProperty $property) => $property->name, $this->properties);
}
/**
* Sucht eine Property nach Namen
*/
public function getProperty(string $name): ?CssProperty
{
foreach ($this->properties as $property) {
if ($property->name === $name) {
return $property;
}
}
return null;
}
/**
* Gibt alle Properties einer bestimmten Kategorie zurück
*/
public function getPropertiesByCategory(CssPropertyCategory $category): array
{
return array_filter(
$this->properties,
fn (CssProperty $property) => $property->getCategory() === $category
);
}
/**
* Prüft ob die Regel einen bestimmten Selektor enthält
*/
public function hasSelector(string $selectorValue): bool
{
foreach ($this->selectors as $selector) {
if ($selector->value === $selectorValue) {
return true;
}
}
return false;
}
/**
* Prüft ob die Regel eine bestimmte Property enthält
*/
public function hasProperty(string $propertyName): bool
{
return $this->getProperty($propertyName) !== null;
}
/**
* Prüft ob die Regel Custom Properties verwendet
*/
public function usesCustomProperties(): bool
{
foreach ($this->properties as $property) {
if ($property->usesCustomProperty()) {
return true;
}
}
return false;
}
/**
* Extrahiert alle Custom Property Referenzen
*/
public function getCustomPropertyReferences(): array
{
$references = [];
foreach ($this->properties as $property) {
$propertyRefs = $property->getCustomPropertyReferences();
$references = array_merge($references, $propertyRefs);
}
return array_unique($references);
}
/**
* Gibt die Regel als CSS-String zurück
*/
public function toCssString(): string
{
$selectors = implode(', ', $this->getSelectorStrings());
$properties = [];
foreach ($this->properties as $property) {
$properties[] = " {$property->name}: {$property->value};";
}
$propertiesString = implode("\n", $properties);
return "$selectors {\n$propertiesString\n}";
}
/**
* Analysiert die Spezifität der Selektoren
*/
public function getSpecificityAnalysis(): array
{
$analysis = [];
foreach ($this->selectors as $selector) {
$analysis[] = [
'selector' => $selector->value,
'specificity' => $selector->calculateSpecificity(),
'type' => $selector->getType()->value,
];
}
return $analysis;
}
/**
* Kategorisiert die Regel basierend auf Selektoren und Properties
*/
public function categorize(): array
{
$selectorTypes = [];
$propertyCategories = [];
foreach ($this->selectors as $selector) {
$selectorTypes[] = $selector->getType()->value;
}
foreach ($this->properties as $property) {
$propertyCategories[] = $property->getCategory()->value;
}
return [
'selector_types' => array_unique($selectorTypes),
'property_categories' => array_unique($propertyCategories),
'uses_custom_properties' => $this->usesCustomProperties(),
'complexity' => $this->calculateComplexity(),
];
}
private function calculateComplexity(): string
{
$selectorCount = count($this->selectors);
$propertyCount = count($this->properties);
$totalSpecificity = array_sum(array_map(
fn (CssSelector $selector) => $selector->calculateSpecificity(),
$this->selectors
));
$complexityScore = $selectorCount + $propertyCount + ($totalSpecificity / 10);
return match(true) {
$complexityScore <= 5 => 'simple',
$complexityScore <= 15 => 'moderate',
default => 'complex'
};
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Repräsentiert einen CSS-Selektor
*/
final readonly class CssSelector
{
public function __construct(
public string $value
) {
}
public static function fromString(string $selector): self
{
return new self(trim($selector));
}
/**
* Berechnet die CSS-Spezifität des Selektors
*/
public function calculateSpecificity(): int
{
$specificity = 0;
// IDs (#id) = 100
preg_match_all('/#[a-zA-Z][\w-]*/', $this->value, $ids);
$specificity += count($ids[0]) * 100;
// Classes (.class), Attributes ([attr]) und Pseudo-Classes (:hover) = 10
preg_match_all('/\.[a-zA-Z][\w-]*/', $this->value, $classes);
$specificity += count($classes[0]) * 10;
preg_match_all('/\[[^\]]*\]/', $this->value, $attributes);
$specificity += count($attributes[0]) * 10;
preg_match_all('/:(?!not\(|where\(|is\()[a-zA-Z][\w-]*/', $this->value, $pseudoClasses);
$specificity += count($pseudoClasses[0]) * 10;
// Elements (div, span) und Pseudo-Elements (::before) = 1
$elements = preg_replace('/[#.\[\]:][^#.\[\]\s]*/', '', $this->value);
preg_match_all('/[a-zA-Z][\w-]*/', $elements, $elementMatches);
$specificity += count($elementMatches[0]);
return $specificity;
}
/**
* Analysiert den Selektor-Typ
*/
public function getType(): CssSelectorType
{
if (str_starts_with($this->value, '.')) {
return CssSelectorType::CLASS_SELECTOR;
}
if (str_starts_with($this->value, '#')) {
return CssSelectorType::ID;
}
if (str_contains($this->value, '[')) {
return CssSelectorType::ATTRIBUTE;
}
if (str_contains($this->value, ':')) {
return CssSelectorType::PSEUDO;
}
if (preg_match('/^[a-zA-Z][\w-]*$/', $this->value)) {
return CssSelectorType::ELEMENT;
}
return CssSelectorType::COMPLEX;
}
/**
* Extrahiert alle CSS-Klassennamen aus dem Selektor
*/
public function extractClasses(): array
{
preg_match_all('/\.([a-zA-Z][\w-]*)/', $this->value, $matches);
return $matches[1] ?? [];
}
/**
* Extrahiert alle IDs aus dem Selektor
*/
public function extractIds(): array
{
preg_match_all('/#([a-zA-Z][\w-]*)/', $this->value, $matches);
return $matches[1] ?? [];
}
/**
* Extrahiert alle Element-Namen aus dem Selektor
*/
public function extractElements(): array
{
$cleaned = preg_replace('/[#.\[\]:][^#.\[\]\s]*/', '', $this->value);
preg_match_all('/[a-zA-Z][\w-]*/', $cleaned, $matches);
return array_filter($matches[0], fn ($element) => ! empty($element));
}
public function toString(): string
{
return $this->value;
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Enum für CSS Selektor Typen
*/
enum CssSelectorType: string
{
case CLASS_SELECTOR = 'class';
case ID = 'id';
case ELEMENT = 'element';
case ATTRIBUTE = 'attribute';
case PSEUDO = 'pseudo';
case COMPLEX = 'complex';
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Repräsentiert eine CSS Custom Property (CSS Variable)
*/
final readonly class CustomProperty
{
public function __construct(
public string $name,
public string $value
) {
}
public static function fromDeclaration(string $declaration): self
{
// Parse "--property-name: value" format
if (preg_match('/--([^:]+):\s*([^;]+)/', $declaration, $matches)) {
return new self(trim($matches[1]), trim($matches[2]));
}
throw new \InvalidArgumentException('Invalid CSS custom property declaration');
}
public function hasValueType(string $type): bool
{
return match($type) {
'color' => $this->isColorValue(),
'size' => $this->isSizeValue(),
'number' => $this->isNumberValue(),
default => false
};
}
public function getValueAs(string $type): mixed
{
return match($type) {
'color' => $this->getColorValue(),
'size' => $this->getSizeValue(),
'number' => $this->getNumberValue(),
default => $this->value
};
}
private function isColorValue(): bool
{
return preg_match('/^(#[0-9a-fA-F]{3,8}|rgb|hsl|oklch|color)/', $this->value) === 1;
}
private function isSizeValue(): bool
{
return preg_match('/^\d+(\.\d+)?(px|em|rem|%|vh|vw)$/', $this->value) === 1;
}
private function isNumberValue(): bool
{
return is_numeric($this->value);
}
private function getColorValue(): CssColor
{
if ($this->isColorValue()) {
$format = match(true) {
str_starts_with($this->value, '#') => ColorFormat::HEX,
str_starts_with($this->value, 'rgb') => ColorFormat::RGB,
str_starts_with($this->value, 'hsl') => ColorFormat::HSL,
str_starts_with($this->value, 'oklch') => ColorFormat::OKLCH,
default => ColorFormat::HEX
};
return new CssColor($this->value, $format);
}
throw new \InvalidArgumentException('Value is not a color');
}
private function getSizeValue(): string
{
return $this->value;
}
private function getNumberValue(): float
{
return (float) $this->value;
}
}

View File

@@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
use App\Framework\Design\Analyzer\ColorAnalysisResult;
use App\Framework\Design\Analyzer\ComponentDetectionResult;
use App\Framework\Design\Analyzer\ConventionCheckResult;
use App\Framework\Design\Analyzer\TokenAnalysisResult;
/**
* Complete Design System Analysis Result
*/
final readonly class DesignSystemAnalysis
{
public function __construct(
public TokenAnalysisResult $tokenAnalysis,
public ColorAnalysisResult $colorAnalysis,
public ComponentDetectionResult $componentAnalysis,
public ConventionCheckResult $conventionAnalysis,
public array $metadata = []
) {
}
public static function empty(): self
{
return new self(
tokenAnalysis: new TokenAnalysisResult(0, [], [], [], [], [], []),
colorAnalysis: new ColorAnalysisResult(0, [], [], [], [], []),
componentAnalysis: new ComponentDetectionResult(0, [], [], [], [], []),
conventionAnalysis: new ConventionCheckResult(100, [], [], [], 'none'),
metadata: []
);
}
/**
* Gets the overall design system maturity level
*/
public function getMaturityLevel(): string
{
$score = $this->getOverallDesignSystemScore();
return match(true) {
$score >= 90 => 'Mature',
$score >= 70 => 'Established',
$score >= 50 => 'Developing',
$score >= 30 => 'Emerging',
default => 'Basic'
};
}
/**
* Calculates overall design system score (0-100)
*/
public function getOverallDesignSystemScore(): float
{
$tokenCoverage = $this->tokenAnalysis->getTokenCoverage();
$tokenScore = $tokenCoverage['usage_percentage'] ?? 0;
$colorScore = $this->colorAnalysis->getConsistencyScore();
$componentScore = $this->componentAnalysis->getConsistencyScore();
$conventionScore = $this->conventionAnalysis->overallScore;
return ($tokenScore + $colorScore + $componentScore + $conventionScore) / 4;
}
/**
* Gets critical issues that need immediate attention
*/
public function getCriticalIssues(): array
{
$issues = [];
// Token issues
$tokenCoverage = $this->tokenAnalysis->getTokenCoverage();
if (($tokenCoverage['usage_percentage'] ?? 0) < 30) {
$issues[] = [
'type' => 'tokens',
'severity' => 'critical',
'message' => 'Very low design token usage',
'impact' => 'Inconsistent styling and maintenance difficulties',
];
}
// Color issues - check contrast compliance
if ($this->colorAnalysis->getContrastComplianceScore() < 80) {
$issues[] = [
'type' => 'colors',
'severity' => 'critical',
'message' => 'WCAG accessibility violations found',
'impact' => 'Poor accessibility for users',
];
}
// Convention issues
if ($this->conventionAnalysis->overallScore < 40) {
$issues[] = [
'type' => 'conventions',
'severity' => 'critical',
'message' => 'Poor naming convention consistency',
'impact' => 'Decreased developer productivity and maintenance',
];
}
return $issues;
}
/**
* Gets quick wins for easy improvements
*/
public function getQuickWins(): array
{
$wins = [];
// Duplicate color removal
if (count($this->colorAnalysis->duplicateColors) > 0) {
$wins[] = [
'type' => 'colors',
'effort' => 'low',
'impact' => 'medium',
'message' => 'Remove duplicate color values',
'action' => 'Consolidate ' . count($this->colorAnalysis->duplicateColors) . ' duplicate colors',
];
}
// Utility class organization based on pattern diversity
if ($this->componentAnalysis->getPatternDiversity() > 80) {
$wins[] = [
'type' => 'components',
'effort' => 'low',
'impact' => 'high',
'message' => 'Standardize component patterns',
'action' => 'Choose one primary CSS methodology for better consistency',
];
}
return $wins;
}
/**
* Gets development roadmap based on analysis
*/
public function getDevelopmentRoadmap(): array
{
$roadmap = [];
$score = $this->getOverallDesignSystemScore();
if ($score < 30) {
$roadmap[] = [
'phase' => 1,
'title' => 'Foundation Setup',
'priority' => 'critical',
'tasks' => [
'Establish core design tokens',
'Define color palette',
'Set naming conventions',
'Create basic component library',
],
'timeline' => '2-4 weeks',
];
}
if ($score >= 30 && $score < 60) {
$roadmap[] = [
'phase' => 2,
'title' => 'System Expansion',
'priority' => 'high',
'tasks' => [
'Expand token coverage',
'Improve component organization',
'Add responsive design tokens',
'Implement consistent spacing scale',
],
'timeline' => '4-6 weeks',
];
}
if ($score >= 60) {
$roadmap[] = [
'phase' => 3,
'title' => 'Optimization & Polish',
'priority' => 'medium',
'tasks' => [
'Optimize token usage',
'Refine component APIs',
'Add advanced theming',
'Implement design system documentation',
],
'timeline' => '6-8 weeks',
];
}
return $roadmap;
}
/**
* Exports analysis as array for JSON serialization
*/
public function exportReport(): array
{
return [
'overall_score' => $this->getOverallDesignSystemScore(),
'maturity_level' => $this->getMaturityLevel(),
'critical_issues' => $this->getCriticalIssues(),
'quick_wins' => $this->getQuickWins(),
'roadmap' => $this->getDevelopmentRoadmap(),
'detailed_analysis' => [
'tokens' => $this->tokenAnalysis->toArray(),
'colors' => $this->colorAnalysis->toArray(),
'components' => $this->componentAnalysis->toArray(),
'conventions' => $this->conventionAnalysis->toArray(),
],
'metadata' => array_merge($this->metadata, [
'generated_at' => date('c'),
'version' => '1.0',
]),
];
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Design Token Value Object
*/
final readonly class DesignToken
{
public function __construct(
public string $name,
public DesignTokenType $type,
public mixed $value,
public string $description,
public array $metadata = []
) {
}
/**
* Factory für Color Token
*/
public static function color(string $name, CssColor $color, string $description = ''): self
{
return new self(
name: $name,
type: DesignTokenType::COLOR,
value: $color,
description: $description ?: "Color: " . ucwords(str_replace(['-', '_'], ' ', $name)),
metadata: ['source' => 'css_custom_property']
);
}
/**
* Factory für Spacing Token
*/
public static function spacing(string $name, string|int $value, string $description = ''): self
{
return new self(
name: $name,
type: DesignTokenType::SPACING,
value: $value,
description: $description ?: "Spacing: " . ucwords(str_replace(['-', '_'], ' ', $name)),
metadata: ['source' => 'css_custom_property']
);
}
/**
* Factory für Typography Token
*/
public static function typography(string $name, mixed $value, string $description = ''): self
{
return new self(
name: $name,
type: DesignTokenType::TYPOGRAPHY,
value: $value,
description: $description ?: "Typography: " . ucwords(str_replace(['-', '_'], ' ', $name)),
metadata: ['source' => 'css_custom_property']
);
}
/**
* Konvertiert zu Array für Export/Serialisierung
*/
public function toArray(): array
{
return [
'name' => $this->name,
'type' => $this->type->value,
'value' => $this->serializeValue(),
'description' => $this->description,
'metadata' => $this->metadata,
];
}
/**
* Erstellt CSS Custom Property String
*/
public function toCssCustomProperty(): string
{
$value = match($this->type) {
DesignTokenType::COLOR => $this->value instanceof CssColor ? $this->value->toString() : (string) $this->value,
default => (string) $this->value
};
return "--{$this->name}: {$value};";
}
/**
* Gibt CSS var() Referenz zurück
*/
public function toCssVar(): string
{
return "var(--{$this->name})";
}
/**
* Prüft ob Token einen bestimmten Wert-Typ hat
*/
public function hasValueType(string $type): bool
{
return match($type) {
'color' => $this->value instanceof CssColor,
'string' => is_string($this->value),
'int' => is_int($this->value),
'float' => is_float($this->value),
'array' => is_array($this->value),
default => false
};
}
/**
* Gibt Wert als bestimmten Typ zurück
*/
public function getValueAs(string $type): mixed
{
return match($type) {
'string' => (string) $this->value,
'int' => (int) $this->value,
'float' => (float) $this->value,
'array' => is_array($this->value) ? $this->value : [$this->value],
'color' => $this->value instanceof CssColor ? $this->value : null,
default => $this->value
};
}
private function serializeValue(): mixed
{
if ($this->value instanceof CssColor) {
return [
'original' => $this->value->originalValue,
'format' => $this->value->format->value,
'hex' => $this->value->toHex(),
'rgb' => $this->value->toRGB()?->toArray(),
];
}
return $this->value;
}
public function toString(): string
{
return $this->toCssVar();
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Enum für Design Token Typen
*/
enum DesignTokenType: string
{
case COLOR = 'color';
case SPACING = 'spacing';
case TYPOGRAPHY = 'typography';
case SHADOW = 'shadow';
case RADIUS = 'radius';
case OPACITY = 'opacity';
case BORDER = 'border';
case ANIMATION = 'animation';
case BREAKPOINT = 'breakpoint';
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Framework\Design\ValueObjects;
/**
* Kategorien für Design Tokens
*/
enum TokenCategory: string
{
case COLOR = 'color';
case TYPOGRAPHY = 'typography';
case SPACING = 'spacing';
case BORDER = 'border';
case SHADOW = 'shadow';
case ANIMATION = 'animation';
case OTHER = 'other';
}