feat: Fix discovery system critical issues

Resolved multiple critical discovery system issues:

## Discovery System Fixes
- Fixed console commands not being discovered on first run
- Implemented fallback discovery for empty caches
- Added context-aware caching with separate cache keys
- Fixed object serialization preventing __PHP_Incomplete_Class

## Cache System Improvements
- Smart caching that only caches meaningful results
- Separate caches for different execution contexts (console, web, test)
- Proper array serialization/deserialization for cache compatibility
- Cache hit logging for debugging and monitoring

## Object Serialization Fixes
- Fixed DiscoveredAttribute serialization with proper string conversion
- Sanitized additional data to prevent object reference issues
- Added fallback for corrupted cache entries

## Performance & Reliability
- All 69 console commands properly discovered and cached
- 534 total discovery items successfully cached and restored
- No more __PHP_Incomplete_Class cache corruption
- Improved error handling and graceful fallbacks

## Testing & Quality
- Fixed code style issues across discovery components
- Enhanced logging for better debugging capabilities
- Improved cache validation and error recovery

Ready for production deployment with stable discovery system.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-13 12:04:17 +02:00
parent 66f7efdcfc
commit 9b74ade5b0
494 changed files with 764014 additions and 1127382 deletions

View File

@@ -129,7 +129,7 @@ final readonly class ComponentDetectionResult
public function getPatternDistribution(): array
{
$total = $this->totalComponents;
if ($total === 0) {
return [
'bem' => 0,
@@ -137,10 +137,10 @@ final readonly class ComponentDetectionResult
'traditional' => 0,
];
}
return [
'bem' => round((count($this->bemComponents) / $total) * 100, 1),
'utility' => round((count($this->utilityComponents) / $total) * 100, 1),
'utility' => round((count($this->utilityComponents) / $total) * 100, 1),
'traditional' => round((count($this->traditionalComponents) / $total) * 100, 1),
];
}

View File

@@ -45,7 +45,7 @@ final readonly class TokenAnalyzer
$unusedTokens = $this->findUnusedTokens($tokens, $tokenUsage);
// Finde verwendete Tokens (alle Tokens minus unbenutzte)
$usedTokens = array_filter($tokens, fn($token) => !in_array($token, $unusedTokens, true));
$usedTokens = array_filter($tokens, fn ($token) => ! in_array($token, $unusedTokens, true));
// Finde fehlende Standard-Tokens
$missingTokens = $this->findMissingStandardTokens($tokensByType);

View File

@@ -14,18 +14,19 @@ final readonly class Component
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) {
@@ -39,7 +40,7 @@ final readonly class Component
default => $this->generateDefaultPreview(),
};
}
private function generateButtonPreview(): string
{
$text = match(true) {
@@ -50,37 +51,37 @@ final readonly class Component
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) {
@@ -90,26 +91,26 @@ final readonly class Component
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

@@ -14,7 +14,7 @@ enum ComponentCategory: string
case LAYOUT = 'layout';
case TYPOGRAPHY = 'typography';
case OTHER = 'other';
public function getDisplayName(): string
{
return match($this) {
@@ -28,7 +28,7 @@ enum ComponentCategory: string
self::OTHER => 'Other Components',
};
}
public function getIcon(): string
{
return match($this) {
@@ -42,4 +42,4 @@ enum ComponentCategory: string
self::OTHER => '🧩',
};
}
}
}

View File

@@ -9,7 +9,7 @@ enum ComponentPattern: string
case BEM = 'bem';
case UTILITY = 'utility';
case TRADITIONAL = 'traditional';
public function getDisplayName(): string
{
return match($this) {
@@ -18,7 +18,7 @@ enum ComponentPattern: string
self::TRADITIONAL => 'Traditional CSS',
};
}
public function getDescription(): string
{
return match($this) {
@@ -27,4 +27,4 @@ enum ComponentPattern: string
self::TRADITIONAL => 'Classic CSS component approach',
};
}
}
}

View File

@@ -8,28 +8,28 @@ 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);
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);
return array_filter($this->components, fn ($c) => $c->pattern === $pattern);
}
public function findByName(string $name): ?Component
{
foreach ($this->components as $component) {
@@ -37,62 +37,62 @@ final readonly class ComponentRegistry
return $component;
}
}
return null;
}
public function getComponentVariants(string $baseName): array
{
return array_filter($this->components, fn($c) => str_starts_with($c->name, $baseName));
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)) {
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 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

@@ -11,7 +11,7 @@ enum ComponentState: string
case FOCUS = 'focus';
case ACTIVE = 'active';
case DISABLED = 'disabled';
public function getDisplayName(): string
{
return match($this) {
@@ -22,7 +22,7 @@ enum ComponentState: string
self::DISABLED => 'Disabled State',
};
}
public function getCssClass(): string
{
return match($this) {
@@ -33,4 +33,4 @@ enum ComponentState: string
self::DISABLED => ':disabled',
};
}
}
}

View File

@@ -19,112 +19,112 @@ final readonly class ComponentScanner
public function scanComponents(array $cssFiles): ComponentRegistry
{
$components = [];
foreach ($cssFiles as $cssFile) {
if (!$cssFile instanceof FilePath) {
if (! $cssFile instanceof FilePath) {
$cssFile = new FilePath($cssFile);
}
if (!$cssFile->exists()) {
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])) {
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];
@@ -133,7 +133,7 @@ final readonly class ComponentScanner
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);
@@ -142,13 +142,13 @@ final readonly class ComponentScanner
$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,
@@ -159,100 +159,100 @@ final readonly class ComponentScanner
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;
}
}
}