- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
398 lines
13 KiB
PHP
398 lines
13 KiB
PHP
<?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;
|
|
}
|
|
}
|