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,120 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
use ArrayIterator;
use Countable;
use IteratorAggregate;
/**
* Type-safe collection of discovered attributes
*/
final readonly class AttributeCollection implements IteratorAggregate, Countable
{
/** @var array<DiscoveredAttribute> */
private array $attributes;
public function __construct(DiscoveredAttribute ...$attributes)
{
$this->attributes = $attributes;
}
public function add(DiscoveredAttribute $attribute): self
{
$newAttributes = [...$this->attributes, $attribute];
return new self(...$newAttributes);
}
/**
* @return ArrayIterator<int, DiscoveredAttribute>
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->attributes);
}
public function count(): int
{
return count($this->attributes);
}
public function isEmpty(): bool
{
return empty($this->attributes);
}
/**
* Filter by attribute class
*/
public function filterByAttributeClass(string $attributeClass): self
{
$filtered = array_filter(
$this->attributes,
fn (DiscoveredAttribute $attr) => $attr->attributeClass === $attributeClass
);
return new self(...$filtered);
}
/**
* Filter by target type
*/
public function filterByTarget(AttributeTarget $target): self
{
$filtered = array_filter(
$this->attributes,
fn (DiscoveredAttribute $attr) => $attr->target === $target
);
return new self(...$filtered);
}
/**
* Get all class attributes
*/
public function getClassAttributes(): self
{
return $this->filterByTarget(AttributeTarget::TARGET_CLASS);
}
/**
* Get all method attributes
*/
public function getMethodAttributes(): self
{
return $this->filterByTarget(AttributeTarget::METHOD);
}
/**
* Convert to array for backwards compatibility
* @deprecated Use object methods instead
* @return array<string, array<array<string, mixed>>>
*/
public function toLegacyArray(): array
{
$result = [];
foreach ($this->attributes as $attribute) {
$attributeClass = $attribute->attributeClass;
if (! isset($result[$attributeClass])) {
$result[$attributeClass] = [];
}
$result[$attributeClass][] = $attribute->toArray();
}
return $result;
}
/**
* @return array<DiscoveredAttribute>
*/
public function toArray(): array
{
return $this->attributes;
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
enum AttributeTarget: string
{
case TARGET_CLASS = 'class';
case METHOD = 'method';
case PROPERTY = 'property';
case PARAMETER = 'parameter';
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
/**
* Cache level enumeration for memory-aware caching
*
* Defines different cache retention and compression strategies
* based on memory pressure conditions.
*/
enum CacheLevel: string
{
case MINIMAL = 'minimal'; // Critical memory: very short TTL, maximum compression
case REDUCED = 'reduced'; // High pressure: reduced TTL, high compression
case COMPRESSED = 'compressed'; // Medium pressure: normal TTL, medium compression
case NORMAL = 'normal'; // Normal memory: standard caching behavior
case EXTENDED = 'extended'; // Low memory usage: extended TTL, no compression
/**
* Get cache retention multiplier for this level
*/
public function getRetentionMultiplier(): float
{
return match ($this) {
self::MINIMAL => 0.1,
self::REDUCED => 0.5,
self::COMPRESSED => 0.75,
self::NORMAL => 1.0,
self::EXTENDED => 1.5
};
}
/**
* Get compression requirement for this level
*/
public function requiresCompression(): bool
{
return match ($this) {
self::MINIMAL, self::REDUCED, self::COMPRESSED => true,
self::NORMAL, self::EXTENDED => false
};
}
/**
* Get default compression level for this cache level
*/
public function getDefaultCompressionLevel(): CompressionLevel
{
return match ($this) {
self::MINIMAL => CompressionLevel::MAXIMUM,
self::REDUCED => CompressionLevel::HIGH,
self::COMPRESSED => CompressionLevel::MEDIUM,
self::NORMAL => CompressionLevel::LOW,
self::EXTENDED => CompressionLevel::NONE
};
}
/**
* Get description of this cache level
*/
public function getDescription(): string
{
return match ($this) {
self::MINIMAL => 'Minimal caching with maximum compression and very short TTL',
self::REDUCED => 'Reduced caching with high compression and short TTL',
self::COMPRESSED => 'Normal caching with medium compression',
self::NORMAL => 'Standard caching behavior',
self::EXTENDED => 'Extended caching with longer TTL and no compression'
};
}
/**
* Check if this level is more aggressive than another
*/
public function isMoreAggressiveThan(self $other): bool
{
$levels = [
self::EXTENDED => 0,
self::NORMAL => 1,
self::COMPRESSED => 2,
self::REDUCED => 3,
self::MINIMAL => 4,
];
return $levels[$this] > $levels[$other];
}
/**
* Suggest cache level based on memory pressure
*/
public static function fromMemoryPressure(float $pressure): self
{
return match (true) {
$pressure >= 0.95 => self::MINIMAL,
$pressure >= 0.85 => self::REDUCED,
$pressure >= 0.75 => self::COMPRESSED,
$pressure >= 0.30 => self::NORMAL,
default => self::EXTENDED
};
}
}

View File

@@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Score;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Discovery\Memory\MemoryStatusInfo;
/**
* Cache metrics value object for memory-aware cache monitoring
*
* Provides comprehensive cache performance and memory usage metrics
* to support intelligent cache management decisions.
*/
final readonly class CacheMetrics
{
public function __construct(
public MemoryStatusInfo $memoryStatus,
public CacheLevel $cacheLevel,
public int $totalItems,
public Score $hitRate,
public Byte $totalSize,
public Score $compressionRatio,
public int $evictionCount,
public Timestamp $timestamp
) {
}
/**
* Get cache efficiency score (combines hit rate with memory efficiency)
*/
public function getEfficiencyScore(): Score
{
// Combine hit rate with memory efficiency
$memoryEfficiency = Score::fromPercentage($this->memoryStatus->memoryPressure)->invert();
return $this->hitRate->combine($memoryEfficiency, 0.7); // 70% hit rate, 30% memory efficiency
}
/**
* Check if cache is performing well
*/
public function isPerformingWell(): bool
{
return $this->hitRate->isHigh() && $this->memoryStatus->memoryPressure->toDecimal() < 0.8;
}
/**
* Get memory pressure impact level
*/
public function getMemoryPressureImpact(): string
{
return match (true) {
$this->memoryStatus->memoryPressure->toDecimal() >= 0.9 => 'critical',
$this->memoryStatus->memoryPressure->toDecimal() >= 0.8 => 'high',
$this->memoryStatus->memoryPressure->toDecimal() >= 0.6 => 'moderate',
default => 'low'
};
}
/**
* Get compression effectiveness
*/
public function getCompressionEffectiveness(): string
{
return match (true) {
$this->compressionRatio->isBelow(Score::fromRatio(3, 10)) => 'excellent', // < 30% (70%+ compression)
$this->compressionRatio->isBelow(Score::medium()) => 'good', // < 50% (50%+ compression)
$this->compressionRatio->isBelow(Score::high()) => 'moderate', // < 70% (30%+ compression)
$this->compressionRatio->isBelow(Score::critical()) => 'low', // < 90% (10%+ compression)
default => 'none' // >= 90% (< 10% compression)
};
}
/**
* Get recommended actions based on metrics
*/
public function getRecommendedActions(): array
{
$actions = [];
// Hit rate recommendations
if ($this->hitRate->isBelow(Score::medium())) {
$actions[] = 'Consider increasing cache TTL or improving cache key strategy';
} elseif ($this->hitRate->isBelow(Score::high())) {
$actions[] = 'Cache hit rate could be improved - review caching patterns';
}
// Memory pressure recommendations
switch ($this->getMemoryPressureImpact()) {
case 'critical':
$actions[] = 'URGENT: Reduce cache size or increase memory limits';
$actions[] = 'Enable maximum compression for all cache items';
break;
case 'high':
$actions[] = 'Consider cache eviction or compression to reduce memory usage';
break;
case 'moderate':
$actions[] = 'Monitor memory usage - consider compression for large items';
break;
}
// Cache level recommendations
if ($this->cacheLevel === CacheLevel::MINIMAL) {
$actions[] = 'Cache is in minimal mode - performance may be impacted';
}
// Eviction recommendations
if ($this->evictionCount > ($this->totalItems * 0.1)) {
$actions[] = 'High eviction rate detected - consider increasing cache size or TTL optimization';
}
return $actions;
}
/**
* Convert to array for logging/debugging
*/
public function toArray(): array
{
return [
'memory_status' => [
'status' => $this->memoryStatus->status->value,
'current_usage' => $this->memoryStatus->currentUsage->toHumanReadable(),
'memory_pressure' => $this->memoryStatus->memoryPressure->toString(),
'available_memory' => $this->memoryStatus->availableMemory->toHumanReadable(),
],
'cache_level' => $this->cacheLevel->value,
'performance' => [
'total_items' => $this->totalItems,
'hit_rate' => $this->hitRate->toPercentage()->toString(),
'total_size' => $this->totalSize->toHumanReadable(),
'compression_ratio' => $this->compressionRatio->toPercentage()->toString(),
'eviction_count' => $this->evictionCount,
],
'analysis' => [
'efficiency_score' => $this->getEfficiencyScore()->toString(),
'is_performing_well' => $this->isPerformingWell(),
'memory_pressure_impact' => $this->getMemoryPressureImpact(),
'compression_effectiveness' => $this->getCompressionEffectiveness(),
],
'recommendations' => $this->getRecommendedActions(),
'timestamp' => $this->timestamp->toFloat(),
];
}
/**
* Calculate estimated memory savings from compression
*/
public function getEstimatedMemorySavings(): Byte
{
if ($this->compressionRatio->value() >= 1.0) {
return Byte::zero();
}
$uncompressedSize = $this->totalSize->divide(max(0.1, $this->compressionRatio->value()));
return $uncompressedSize->subtract($this->totalSize);
}
/**
* Get cache health status
*/
public function getHealthStatus(): string
{
return match (true) {
$this->isPerformingWell() => 'healthy',
$this->hitRate->isAbove(Score::medium()) && $this->memoryStatus->memoryPressure->toDecimal() < 0.9 => 'warning',
default => 'critical'
};
}
/**
* Get overall cache quality score
*/
public function getQualityScore(): Score
{
// Combine multiple factors for overall quality
$hitRateScore = $this->hitRate;
$memoryScore = Score::fromPercentage($this->memoryStatus->memoryPressure)->invert();
$compressionScore = $this->compressionRatio->invert(); // Better compression = higher score
$evictionScore = $this->evictionCount > 0
? Score::fromRatio(max(0, $this->totalItems - $this->evictionCount), $this->totalItems)
: Score::max();
return Score::weightedAverage(
[$hitRateScore, $memoryScore, $compressionScore, $evictionScore],
[0.4, 0.3, 0.2, 0.1] // Hit rate most important, then memory, compression, eviction
);
}
}

View File

@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
/**
* Cache tier enumeration for tiered caching strategy
*
* Defines different cache tiers with specific retention, compression,
* and access characteristics optimized for different usage patterns.
*/
enum CacheTier: string
{
case HOT = 'hot'; // Frequently accessed, small items, no compression
case WARM = 'warm'; // Moderately accessed, light compression
case COLD = 'cold'; // Rarely accessed, medium compression
case ARCHIVE = 'archive'; // Very rarely accessed, maximum compression
/**
* Get compression level for this tier
*/
public function getCompressionLevel(): CompressionLevel
{
return match ($this) {
self::HOT => CompressionLevel::NONE,
self::WARM => CompressionLevel::LOW,
self::COLD => CompressionLevel::MEDIUM,
self::ARCHIVE => CompressionLevel::MAXIMUM
};
}
/**
* Get TTL multiplier for this tier
*/
public function getTtlMultiplier(): float
{
return match ($this) {
self::HOT => 0.5, // Shorter TTL for hot data
self::WARM => 1.0, // Normal TTL
self::COLD => 2.0, // Longer TTL for cold data
self::ARCHIVE => 5.0 // Very long TTL for archived data
};
}
/**
* Get expected access frequency for this tier
*/
public function getExpectedAccessFrequency(): float
{
return match ($this) {
self::HOT => 20.0, // 20+ accesses per hour
self::WARM => 5.0, // 5-20 accesses per hour
self::COLD => 1.0, // 1-5 accesses per hour
self::ARCHIVE => 0.1 // < 1 access per hour
};
}
/**
* Get maximum item size for this tier
*/
public function getMaxItemSize(): int
{
return match ($this) {
self::HOT => 10 * 1024, // 10KB
self::WARM => 100 * 1024, // 100KB
self::COLD => 1024 * 1024, // 1MB
self::ARCHIVE => PHP_INT_MAX // No limit
};
}
/**
* Get priority for this tier (higher = more important)
*/
public function getPriority(): int
{
return match ($this) {
self::HOT => 100,
self::WARM => 75,
self::COLD => 50,
self::ARCHIVE => 25
};
}
/**
* Get higher tier (for promotion)
*/
public function getHigherTier(): ?self
{
return match ($this) {
self::ARCHIVE => self::COLD,
self::COLD => self::WARM,
self::WARM => self::HOT,
self::HOT => null // Already highest
};
}
/**
* Get lower tier (for demotion)
*/
public function getLowerTier(): ?self
{
return match ($this) {
self::HOT => self::WARM,
self::WARM => self::COLD,
self::COLD => self::ARCHIVE,
self::ARCHIVE => null // Already lowest
};
}
/**
* Check if this tier should be used under memory pressure
*/
public function isRecommendedUnderMemoryPressure(float $pressure): bool
{
return match ($this) {
self::HOT => $pressure < 0.6, // Only under low pressure
self::WARM => $pressure < 0.8, // Under moderate pressure
self::COLD => $pressure < 0.9, // Under high pressure
self::ARCHIVE => true // Always acceptable
};
}
/**
* Get description of this tier
*/
public function getDescription(): string
{
return match ($this) {
self::HOT => 'High-frequency access, minimal compression, short TTL',
self::WARM => 'Moderate access, light compression, normal TTL',
self::COLD => 'Low-frequency access, medium compression, long TTL',
self::ARCHIVE => 'Rare access, maximum compression, very long TTL'
};
}
/**
* Convert to corresponding cache level
*/
public function toCacheLevel(): CacheLevel
{
return match ($this) {
self::HOT => CacheLevel::EXTENDED,
self::WARM => CacheLevel::NORMAL,
self::COLD => CacheLevel::COMPRESSED,
self::ARCHIVE => CacheLevel::MINIMAL
};
}
/**
* Suggest tier based on data characteristics
*/
public static function suggest(int $dataSize, float $accessFrequency, float $memoryPressure): self
{
// Under high memory pressure, prefer lower tiers
if ($memoryPressure > 0.85) {
return $dataSize > 100 * 1024 ? self::ARCHIVE : self::COLD;
}
// Normal tier assignment
return match (true) {
$dataSize <= 10 * 1024 && $accessFrequency >= 20 => self::HOT,
$dataSize <= 100 * 1024 && $accessFrequency >= 5 => self::WARM,
$dataSize <= 1024 * 1024 && $accessFrequency >= 1 => self::COLD,
default => self::ARCHIVE
};
}
/**
* Get all tiers ordered by priority (highest first)
*/
public static function orderedByPriority(): array
{
return [self::HOT, self::WARM, self::COLD, self::ARCHIVE];
}
/**
* Get memory-efficient tiers for cleanup
*/
public static function getMemoryEfficientTiers(): array
{
return [self::ARCHIVE, self::COLD]; // Most compressed tiers
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
/**
* Compression level enumeration for cache data compression
*
* Defines different compression strategies with trade-offs between
* compression ratio and CPU usage.
*/
enum CompressionLevel: string
{
case NONE = 'none'; // No compression
case LOW = 'low'; // Fast compression, lower ratio
case MEDIUM = 'medium'; // Balanced compression
case HIGH = 'high'; // Good compression, higher CPU
case MAXIMUM = 'maximum'; // Best compression, highest CPU
/**
* Get the gzip compression level integer
*/
public function getGzipLevel(): int
{
return match ($this) {
self::NONE => 0,
self::LOW => 1,
self::MEDIUM => 6,
self::HIGH => 8,
self::MAXIMUM => 9
};
}
/**
* Get expected compression ratio (approximate)
*/
public function getExpectedRatio(): float
{
return match ($this) {
self::NONE => 1.0, // No compression
self::LOW => 0.8, // 20% compression
self::MEDIUM => 0.6, // 40% compression
self::HIGH => 0.4, // 60% compression
self::MAXIMUM => 0.3 // 70% compression
};
}
/**
* Get CPU cost relative to no compression
*/
public function getCpuCostMultiplier(): float
{
return match ($this) {
self::NONE => 1.0,
self::LOW => 1.2,
self::MEDIUM => 1.5,
self::HIGH => 2.0,
self::MAXIMUM => 3.0
};
}
/**
* Get minimum data size threshold for this compression level
*/
public function getMinimumDataSize(): int
{
return match ($this) {
self::NONE => 0,
self::LOW => 1024, // 1KB
self::MEDIUM => 512, // 512 bytes
self::HIGH => 256, // 256 bytes
self::MAXIMUM => 128 // 128 bytes
};
}
/**
* Check if compression is beneficial for given data size
*/
public function isBeneficialFor(int $dataSize): bool
{
return $this !== self::NONE && $dataSize >= $this->getMinimumDataSize();
}
/**
* Get description of this compression level
*/
public function getDescription(): string
{
return match ($this) {
self::NONE => 'No compression applied',
self::LOW => 'Fast compression with minimal CPU overhead',
self::MEDIUM => 'Balanced compression with moderate CPU usage',
self::HIGH => 'Good compression with higher CPU cost',
self::MAXIMUM => 'Maximum compression with highest CPU cost'
};
}
/**
* Suggest compression level based on data size and memory pressure
*/
public static function suggest(int $dataSize, float $memoryPressure): self
{
if ($dataSize < 128) {
return self::NONE; // Too small to compress effectively
}
return match (true) {
$memoryPressure >= 0.95 => self::MAXIMUM,
$memoryPressure >= 0.85 => self::HIGH,
$memoryPressure >= 0.75 => self::MEDIUM,
$memoryPressure >= 0.50 => self::LOW,
default => self::NONE
};
}
}

View File

@@ -0,0 +1,213 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\MethodName;
use App\Framework\Filesystem\FilePath;
use Attribute;
/**
* Represents a discovered attribute with all its metadata
*/
final readonly class DiscoveredAttribute
{
/**
* @param array<string, mixed> $arguments
* @param array<string, mixed> $additionalData
*/
public function __construct(
public ClassName $className,
public string $attributeClass,
public AttributeTarget $target,
public ?MethodName $methodName = null,
public ?string $propertyName = null,
public array $arguments = [],
public ?FilePath $filePath = null,
public array $additionalData = []
) {
}
public function isClassAttribute(): bool
{
return $this->target === AttributeTarget::TARGET_CLASS;
}
public function isMethodAttribute(): bool
{
return $this->target === AttributeTarget::METHOD;
}
public function isPropertyAttribute(): bool
{
return $this->target === AttributeTarget::PROPERTY;
}
/**
* Create from legacy array format (for cache compatibility)
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
// Handle both new DiscoveredAttribute format and old AttributeMapping format
$classData = $data['class'] ?? $data['className'] ?? '';
if (is_object($classData)) {
// Check if it's a complete ClassName object or incomplete class
if ($classData instanceof ClassName) {
$className = $classData;
} elseif (is_object($classData) && method_exists($classData, 'getFullyQualified')) {
try {
$className = ClassName::create($classData->getFullyQualified());
} catch (\Throwable) {
// Fallback: try to extract from object properties or use empty string
$className = ClassName::create('');
}
} else {
// Incomplete class or other object - skip or use fallback
$className = ClassName::create('');
}
} else {
$className = ClassName::create($classData);
}
$attributeClass = $data['attribute_class'] ?? $data['attributeClass'] ?? $data['attribute'] ?? '';
$target = AttributeTarget::from($data['target_type'] ?? $data['target'] ?? 'class');
$methodName = null;
$methodData = $data['method'] ?? $data['methodName'] ?? null;
if ($methodData !== null) {
if (is_object($methodData)) {
// Check if it's a complete MethodName object
if ($methodData instanceof MethodName) {
$methodName = $methodData;
} elseif (method_exists($methodData, 'toString')) {
try {
$methodName = MethodName::create($methodData->toString());
} catch (\Throwable) {
$methodName = null;
}
}
} else {
$methodName = MethodName::create($methodData);
}
}
$filePath = null;
$fileData = $data['file'] ?? $data['filePath'] ?? null;
if ($fileData !== null && ! empty($fileData)) {
if (is_object($fileData)) {
// Check if it's a complete FilePath object
if ($fileData instanceof FilePath) {
$filePath = $fileData;
} elseif (method_exists($fileData, 'toString')) {
try {
$filePath = FilePath::create($fileData->toString());
} catch (\Throwable) {
$filePath = null;
}
}
} else {
$filePath = FilePath::create($fileData);
}
}
$arguments = $data['arguments'] ?? [];
$additionalData = $data;
// Remove standard fields from additionalData
unset($additionalData['class'], $additionalData['attribute'], $additionalData['attribute_class']);
unset($additionalData['target'], $additionalData['target_type'], $additionalData['method']);
unset($additionalData['file'], $additionalData['arguments']);
return new self(
className: $className,
attributeClass: $attributeClass,
target: $target,
methodName: $methodName,
propertyName: $data['property'] ?? null,
arguments: $arguments,
filePath: $filePath,
additionalData: $additionalData
);
}
/**
* Get unique identifier for deduplication
*/
public function getUniqueId(): string
{
$base = $this->className->getFullyQualified() . '::' . $this->attributeClass;
if ($this->methodName !== null) {
$base .= '::' . $this->methodName->toString();
} elseif ($this->propertyName !== null) {
$base .= '::$' . $this->propertyName;
}
return $base;
}
/**
* Create an instance of the attribute class with the stored arguments
*/
public function createAttributeInstance(): ?object
{
try {
// Use PHP 8's named arguments with unpacking
return new $this->attributeClass(...$this->arguments);
} catch (\Throwable) {
return null;
}
}
/**
* Get memory footprint estimate
*/
public function getMemoryFootprint(): Byte
{
$bytes = strlen($this->className->getFullyQualified()) +
strlen($this->attributeClass) +
($this->methodName ? strlen($this->methodName->toString()) : 0) +
($this->propertyName ?? 0) +
($this->filePath ? strlen($this->filePath->toString()) : 0) +
strlen($this->target->value) +
(count($this->arguments) * 50) + // Rough estimate
(count($this->additionalData) * 30); // Rough estimate
return Byte::fromBytes($bytes);
}
/**
* Convert to array for backwards compatibility
* @deprecated Use object properties instead
*/
public function toArray(): array
{
$data = [
'class' => $this->className->getFullyQualified(),
'attribute_class' => $this->attributeClass,
'target_type' => $this->target->value,
];
if ($this->methodName !== null) {
$data['method'] = $this->methodName->toString();
}
if ($this->propertyName !== null) {
$data['property'] = $this->propertyName;
}
if (! empty($this->arguments)) {
$data['arguments'] = $this->arguments;
}
if ($this->filePath !== null) {
$data['file'] = $this->filePath->toString();
}
return array_merge($data, $this->additionalData);
}
}

View File

@@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
use App\Framework\Core\ValueObjects\Duration;
/**
* Configuration value object for Discovery service
*
* Encapsulates all discovery-related configuration in a single immutable object
* to reduce constructor complexity and improve maintainability.
*/
final readonly class DiscoveryConfiguration
{
public readonly Duration $cacheTimeout;
public function __construct(
public array $paths = [],
public array $attributeMappers = [],
public array $targetInterfaces = [],
public bool $useCache = true,
?Duration $cacheTimeout = null,
public ?string $contextSuffix = null,
public int $memoryLimitMB = 128,
public bool $enableEventDispatcher = true,
public bool $enableMemoryMonitoring = true,
public bool $enablePerformanceTracking = true,
public int $maxFilesPerBatch = 100,
public float $memoryPressureThreshold = 0.8
) {
// Set default cache timeout if not provided
$this->cacheTimeout = $cacheTimeout ?? Duration::fromHours(24);
}
/**
* Create configuration for development environment
*/
public static function development(): self
{
return new self(
useCache : true, // Disable cache for development
memoryLimitMB : 512, // Much higher memory limit for development
enableMemoryMonitoring : false,
enablePerformanceTracking: true, // Disable memory monitoring for development to avoid false positives
maxFilesPerBatch : 500, // Larger batches to ensure complete discovery
memoryPressureThreshold : 0.95 // Much more relaxed memory pressure threshold for development
);
}
/**
* Create configuration for production environment
*/
public static function production(): self
{
return new self(
useCache: true,
cacheTimeout: Duration::fromHours(24),
memoryLimitMB: 128,
enablePerformanceTracking: false, // Disable for performance
maxFilesPerBatch: 200 // Larger batches for better performance
);
}
/**
* Create configuration for testing environment
*/
public static function testing(): self
{
return new self(
useCache: false,
memoryLimitMB: 64,
enableEventDispatcher: false, // Disable events for testing
enableMemoryMonitoring: false,
enablePerformanceTracking: false,
maxFilesPerBatch: 25
);
}
/**
* Create configuration with specific paths (factory method)
*/
public static function forPaths(array $paths): self
{
return new self(paths: $paths);
}
/**
* Create configuration with specific mappers
*/
public static function withMappers(array $attributeMappers, array $targetInterfaces = []): self
{
return new self(
attributeMappers: $attributeMappers,
targetInterfaces: $targetInterfaces
);
}
/**
* Create a new configuration with modified cache settings
*/
public function withCache(bool $useCache, ?Duration $timeout = null): self
{
return new self(
paths: $this->paths,
attributeMappers: $this->attributeMappers,
targetInterfaces: $this->targetInterfaces,
useCache: $useCache,
cacheTimeout: $timeout ?? $this->cacheTimeout,
contextSuffix: $this->contextSuffix,
memoryLimitMB: $this->memoryLimitMB,
enableEventDispatcher: $this->enableEventDispatcher,
enableMemoryMonitoring: $this->enableMemoryMonitoring,
enablePerformanceTracking: $this->enablePerformanceTracking,
maxFilesPerBatch: $this->maxFilesPerBatch,
memoryPressureThreshold: $this->memoryPressureThreshold
);
}
/**
* Create a new configuration with modified memory settings
*/
public function withMemorySettings(int $limitMB, float $pressureThreshold = 0.8): self
{
return new self(
paths: $this->paths,
attributeMappers: $this->attributeMappers,
targetInterfaces: $this->targetInterfaces,
useCache: $this->useCache,
cacheTimeout: $this->cacheTimeout,
contextSuffix: $this->contextSuffix,
memoryLimitMB: $limitMB,
enableEventDispatcher: $this->enableEventDispatcher,
enableMemoryMonitoring: $this->enableMemoryMonitoring,
enablePerformanceTracking: $this->enablePerformanceTracking,
maxFilesPerBatch: $this->maxFilesPerBatch,
memoryPressureThreshold: $pressureThreshold
);
}
/**
* Create a new configuration with modified context suffix
*/
public function withContextSuffix(string $suffix): self
{
return new self(
paths: $this->paths,
attributeMappers: $this->attributeMappers,
targetInterfaces: $this->targetInterfaces,
useCache: $this->useCache,
cacheTimeout: $this->cacheTimeout,
contextSuffix: $suffix,
memoryLimitMB: $this->memoryLimitMB,
enableEventDispatcher: $this->enableEventDispatcher,
enableMemoryMonitoring: $this->enableMemoryMonitoring,
enablePerformanceTracking: $this->enablePerformanceTracking,
maxFilesPerBatch: $this->maxFilesPerBatch,
memoryPressureThreshold: $this->memoryPressureThreshold
);
}
/**
* Create a new configuration with modified paths
*/
public function withPaths(array $paths): self
{
return new self(
paths: $paths,
attributeMappers: $this->attributeMappers,
targetInterfaces: $this->targetInterfaces,
useCache: $this->useCache,
cacheTimeout: $this->cacheTimeout,
contextSuffix: $this->contextSuffix,
memoryLimitMB: $this->memoryLimitMB,
enableEventDispatcher: $this->enableEventDispatcher,
enableMemoryMonitoring: $this->enableMemoryMonitoring,
enablePerformanceTracking: $this->enablePerformanceTracking,
maxFilesPerBatch: $this->maxFilesPerBatch,
memoryPressureThreshold: $this->memoryPressureThreshold
);
}
/**
* Validate configuration settings
*/
public function validate(): void
{
if ($this->memoryLimitMB < 32) {
throw new \InvalidArgumentException('Memory limit must be at least 32MB');
}
if ($this->memoryPressureThreshold < 0.1 || $this->memoryPressureThreshold > 1.0) {
throw new \InvalidArgumentException('Memory pressure threshold must be between 0.1 and 1.0');
}
if ($this->maxFilesPerBatch < 1) {
throw new \InvalidArgumentException('Max files per batch must be at least 1');
}
if ($this->cacheTimeout->toSeconds() < 60) {
throw new \InvalidArgumentException('Cache timeout must be at least 60 seconds');
}
}
/**
* Get memory limit in bytes
*/
public function getMemoryLimitBytes(): int
{
return $this->memoryLimitMB * 1024 * 1024;
}
/**
* Check if feature is enabled
*/
public function isFeatureEnabled(string $feature): bool
{
return match ($feature) {
'cache' => $this->useCache,
'events' => $this->enableEventDispatcher,
'memory_monitoring' => $this->enableMemoryMonitoring,
'performance_tracking' => $this->enablePerformanceTracking,
default => false
};
}
/**
* Convert to array for debugging/logging
*/
public function toArray(): array
{
return [
'paths_count' => count($this->paths),
'attribute_mappers_count' => count($this->attributeMappers),
'target_interfaces_count' => count($this->targetInterfaces),
'use_cache' => $this->useCache,
'cache_timeout_seconds' => $this->cacheTimeout->toSeconds(),
'context_suffix' => $this->contextSuffix,
'memory_limit_mb' => $this->memoryLimitMB,
'enable_event_dispatcher' => $this->enableEventDispatcher,
'enable_memory_monitoring' => $this->enableMemoryMonitoring,
'enable_performance_tracking' => $this->enablePerformanceTracking,
'max_files_per_batch' => $this->maxFilesPerBatch,
'memory_pressure_threshold' => $this->memoryPressureThreshold,
];
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\Discovery\Cache\DiscoveryCacheIdentifiers;
use DateTimeImmutable;
/**
* Context information for the discovery process
*/
final class DiscoveryContext
{
private int $processedFiles = 0;
private array $metrics = [];
public function __construct(
public readonly array $paths,
public readonly ScanType $scanType,
public readonly DiscoveryOptions $options,
public readonly DateTimeImmutable $startTime
) {
}
public function incrementProcessedFiles(): void
{
$this->processedFiles++;
}
public function getProcessedFiles(): int
{
return $this->processedFiles;
}
public function addMetric(string $key, mixed $value): void
{
$this->metrics[$key] = $value;
}
public function getMetrics(): array
{
return $this->metrics;
}
public function getDuration(Clock $clock): Duration
{
$endTime = $clock->now();
$diff = $endTime->getTimestamp() - $this->startTime->getTimestamp();
return Duration::fromSeconds($diff);
}
public function getCacheKey(): CacheKey
{
return DiscoveryCacheIdentifiers::discoveryKey($this->paths, $this->scanType);
}
public function isIncremental(): bool
{
return $this->scanType === ScanType::INCREMENTAL;
}
public function shouldUseCache(): bool
{
return $this->options->useCache;
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
/**
* Configuration options for discovery process
*/
final readonly class DiscoveryOptions
{
public function __construct(
public ScanType $scanType = ScanType::FULL,
public array $paths = ['/src'],
public bool $useCache = true,
public bool $parallel = false,
public int $batchSize = 50,
public bool $showProgress = false,
public array $excludePatterns = [],
public array $includePatterns = ['*.php']
) {
}
public static function defaults(): self
{
return new self();
}
public function withScanType(ScanType $scanType): self
{
return new self(
scanType: $scanType,
paths: $this->paths,
useCache: $this->useCache,
parallel: $this->parallel,
batchSize: $this->batchSize,
showProgress: $this->showProgress,
excludePatterns: $this->excludePatterns,
includePatterns: $this->includePatterns
);
}
public function withPaths(array $paths): self
{
return new self(
scanType: $this->scanType,
paths: $paths,
useCache: $this->useCache,
parallel: $this->parallel,
batchSize: $this->batchSize,
showProgress: $this->showProgress,
excludePatterns: $this->excludePatterns,
includePatterns: $this->includePatterns
);
}
public function withoutCache(): self
{
return new self(
scanType: $this->scanType,
paths: $this->paths,
useCache: false,
parallel: $this->parallel,
batchSize: $this->batchSize,
showProgress: $this->showProgress,
excludePatterns: $this->excludePatterns,
includePatterns: $this->includePatterns
);
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Filesystem\File;
use App\Framework\Filesystem\FilePath;
/**
* Context information for file processing
*/
final readonly class FileContext
{
/** @var array<ClassName> */
private array $classNames;
public function __construct(
public File $file,
public FilePath $path,
array $classNames = []
) {
$this->classNames = $classNames;
}
public static function fromFile(File $file): self
{
return new self(
file: $file,
path: $file->getPath(),
classNames: []
);
}
/**
* @param array<ClassName> $classNames
*/
public function withClassNames(array $classNames): self
{
return new self(
file: $this->file,
path: $this->path,
classNames: $classNames
);
}
/**
* @return array<ClassName>
*/
public function getClassNames(): array
{
return $this->classNames;
}
public function hasClasses(): bool
{
return ! empty($this->classNames);
}
public function getNamespace(): ?string
{
if (empty($this->classNames)) {
return null;
}
return $this->classNames[0]->getNamespace();
}
/**
* Get all fully qualified class names
* @return array<string>
*/
public function getFullyQualifiedNames(): array
{
return array_map(
fn (ClassName $className) => $className->getFullyQualified(),
$this->classNames
);
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Filesystem\FilePath;
/**
* Immutable value object for interface implementation mappings
* Replaces simple arrays with memory-efficient typed structure
*/
final readonly class InterfaceMapping
{
public function __construct(
public ClassName $interface,
public ClassName $implementation,
public FilePath $file
) {
}
public static function create(
string $interface,
string $implementation,
string $file
): self {
return new self(
interface: ClassName::create($interface),
implementation: ClassName::create($implementation),
file: FilePath::create($file)
);
}
/**
* Get unique identifier for deduplication
*/
public function getUniqueId(): string
{
return $this->interface->getFullyQualified() . '::' . $this->implementation->getFullyQualified();
}
/**
* Check if this mapping is the same as another
*/
public function isSameAs(self $other): bool
{
return $this->interface->equals($other->interface) &&
$this->implementation->equals($other->implementation);
}
/**
* Check if this implements the given interface
*/
public function implementsInterface(ClassName $interface): bool
{
return $this->interface->equals($interface);
}
/**
* Get memory footprint estimate
*/
public function getMemoryFootprint(): Byte
{
$bytes = strlen($this->interface->getFullyQualified()) +
strlen($this->implementation->getFullyQualified()) +
strlen($this->file->toString());
return Byte::fromBytes($bytes);
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\GrowthRate;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Information about a detected memory leak
*/
final readonly class MemoryLeakInfo
{
public function __construct(
public string $operation,
public Byte $startMemory,
public Byte $endMemory,
public Byte $growth,
public GrowthRate $growthPercentage,
public Timestamp $timestamp
) {
}
public function isCritical(): bool
{
// Critical if growth > 10MB or > 200%
return $this->growth->greaterThan(Byte::fromMegabytes(10))
|| $this->growthPercentage->isCriticalMemoryGrowth();
}
public function isSignificant(): bool
{
// Significant if growth > 5MB or > 50%
return $this->growth->greaterThan(Byte::fromMegabytes(5))
|| $this->growthPercentage->isHighMemoryGrowth();
}
public function __toString(): string
{
return sprintf(
'%s: %s -> %s (growth: %s / %s%%)',
$this->operation,
$this->startMemory->toHumanReadable(),
$this->endMemory->toHumanReadable(),
$this->growth->toHumanReadable(),
$this->growthPercentage->getValue()
);
}
public function toArray(): array
{
return [
'operation' => $this->operation,
'start_memory' => $this->startMemory->toBytes(),
'start_memory_human' => $this->startMemory->toHumanReadable(),
'end_memory' => $this->endMemory->toBytes(),
'end_memory_human' => $this->endMemory->toHumanReadable(),
'growth' => $this->growth->toBytes(),
'growth_human' => $this->growth->toHumanReadable(),
'growth_percentage' => $this->growthPercentage->getValue(),
'is_critical' => $this->isCritical(),
'is_significant' => $this->isSignificant(),
'timestamp' => $this->timestamp->toFloat(),
'datetime' => $this->timestamp->toDateTime()->format('Y-m-d H:i:s'),
];
}
}

View File

@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
/**
* Memory management strategies for Discovery operations
*
* Defines different approaches to memory management based on
* system constraints and operation requirements.
*/
enum MemoryStrategy: string
{
/**
* Adaptive strategy - adjusts based on memory pressure
* Monitors memory usage and adapts chunk sizes dynamically
*/
case ADAPTIVE = 'adaptive';
/**
* Conservative strategy - prioritizes memory conservation
* Uses small chunks and frequent cleanup
*/
case CONSERVATIVE = 'conservative';
/**
* Aggressive strategy - prioritizes speed over memory
* Uses large chunks and minimal cleanup
*/
case AGGRESSIVE = 'aggressive';
/**
* Streaming strategy - minimal memory footprint
* Processes items one by one with immediate cleanup
*/
case STREAMING = 'streaming';
/**
* Batch strategy - balanced approach
* Fixed batch sizes with predictable memory usage
*/
case BATCH = 'batch';
/**
* Get default chunk size for this strategy
*/
public function getDefaultChunkSize(): int
{
return match ($this) {
self::ADAPTIVE => 100, // Will be adjusted based on memory pressure
self::CONSERVATIVE => 25, // Small chunks
self::AGGRESSIVE => 500, // Large chunks
self::STREAMING => 1, // One item at a time
self::BATCH => 100, // Standard batch size
};
}
/**
* Get memory pressure threshold for this strategy
*/
public function getMemoryPressureThreshold(): float
{
return match ($this) {
self::ADAPTIVE => 0.7, // Adapt when 70% memory used
self::CONSERVATIVE => 0.5, // Conservative at 50%
self::AGGRESSIVE => 0.9, // Only care at 90%
self::STREAMING => 0.8, // Moderate threshold
self::BATCH => 0.75, // Standard threshold
};
}
/**
* Get cleanup frequency for this strategy
*/
public function getCleanupFrequency(): int
{
return match ($this) {
self::ADAPTIVE => 50, // Every 50 items (adaptive)
self::CONSERVATIVE => 10, // Frequent cleanup
self::AGGRESSIVE => 200, // Minimal cleanup
self::STREAMING => 1, // Cleanup after each item
self::BATCH => 100, // Standard cleanup
};
}
/**
* Check if strategy supports dynamic adjustment
*/
public function supportsDynamicAdjustment(): bool
{
return match ($this) {
self::ADAPTIVE => true,
self::CONSERVATIVE,
self::AGGRESSIVE,
self::STREAMING,
self::BATCH => false,
};
}
/**
* Get strategy description for logging/debugging
*/
public function getDescription(): string
{
return match ($this) {
self::ADAPTIVE => 'Dynamically adjusts based on memory pressure and system performance',
self::CONSERVATIVE => 'Prioritizes memory conservation with small chunks and frequent cleanup',
self::AGGRESSIVE => 'Prioritizes speed with large chunks and minimal cleanup overhead',
self::STREAMING => 'Minimal memory footprint processing items one by one',
self::BATCH => 'Balanced approach with fixed batch sizes and predictable memory usage',
};
}
/**
* Suggest strategy based on system constraints
*/
public static function suggestForSystem(int $availableMemoryMB, int $itemCount): self
{
// Very low memory systems
if ($availableMemoryMB < 64) {
return self::STREAMING;
}
// Low memory or high item count
if ($availableMemoryMB < 128 || $itemCount > 10000) {
return self::CONSERVATIVE;
}
// High memory systems with moderate item count
if ($availableMemoryMB > 512 && $itemCount < 1000) {
return self::AGGRESSIVE;
}
// Variable workloads
if ($itemCount > 5000) {
return self::ADAPTIVE;
}
// Default balanced approach
return self::BATCH;
}
/**
* Suggest strategy based on discovery configuration
*/
public static function suggestForDiscovery(
int $estimatedFileCount,
int $memoryLimitMB,
bool $enablePerformanceTracking = false
): self {
// Performance tracking adds memory overhead
$effectiveMemory = $enablePerformanceTracking
? (int)($memoryLimitMB * 0.8)
: $memoryLimitMB;
// Large codebases need adaptive or conservative approaches
if ($estimatedFileCount > 5000) {
return $effectiveMemory > 256 ? self::ADAPTIVE : self::CONSERVATIVE;
}
// Medium codebases can use batch processing
if ($estimatedFileCount > 1000) {
return $effectiveMemory > 128 ? self::BATCH : self::CONSERVATIVE;
}
// Small codebases can afford aggressive processing
if ($estimatedFileCount < 500) {
return $effectiveMemory > 64 ? self::AGGRESSIVE : self::BATCH;
}
// Default to adaptive for unknown workloads
return self::ADAPTIVE;
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Filesystem\FilePath;
use App\Framework\Reflection\WrappedReflectionClass;
/**
* Context information for reflection-based discovery
*/
final readonly class ReflectionContext
{
public function __construct(
public ClassName $className,
public FilePath $filePath,
public WrappedReflectionClass $reflection
) {
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
/**
* Configurable scan strategies for different use cases
*/
enum ScanStrategy: string
{
case DEPTH_FIRST = 'depth_first';
case BREADTH_FIRST = 'breadth_first';
case PRIORITY_BASED = 'priority_based';
case MEMORY_OPTIMIZED = 'memory_optimized';
case PERFORMANCE_OPTIMIZED = 'performance_optimized';
case FIBER_PARALLEL = 'fiber_parallel';
public function getDescription(): string
{
return match($this) {
self::DEPTH_FIRST => 'Scan deeply into directory structures first',
self::BREADTH_FIRST => 'Scan all directories at same level before going deeper',
self::PRIORITY_BASED => 'Scan important directories first (src, app, etc.)',
self::MEMORY_OPTIMIZED => 'Optimize for low memory usage, process small files first',
self::PERFORMANCE_OPTIMIZED => 'Optimize for speed, use parallel processing',
self::FIBER_PARALLEL => 'Use Fibers for parallel directory processing',
};
}
/**
* Get recommended chunk size for this strategy
*/
public function getRecommendedChunkSize(): int
{
return match($this) {
self::MEMORY_OPTIMIZED => 50,
self::PERFORMANCE_OPTIMIZED => 200,
default => 100,
};
}
/**
* Should use parallel processing?
*/
public function useParallelProcessing(): bool
{
return match($this) {
self::PERFORMANCE_OPTIMIZED, self::FIBER_PARALLEL => true,
default => false,
};
}
/**
* Should use Fiber-based parallel processing?
*/
public function useFiberProcessing(): bool
{
return $this === self::FIBER_PARALLEL;
}
/**
* Get priority directories for PRIORITY_BASED strategy
*/
public function getPriorityDirectories(): array
{
return [
'src',
'app',
'lib',
'core',
'domain',
'application',
'infrastructure',
'tests',
'spec',
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
/**
* Type of discovery scan being performed
*/
enum ScanType: string
{
case FULL = 'full';
case INCREMENTAL = 'incremental';
case PARTIAL = 'partial';
case SELECTIVE = 'selective';
case RECOVERY = 'recovery';
public function getDescription(): string
{
return match($this) {
self::FULL => 'Complete scan of all directories',
self::INCREMENTAL => 'Scan only changed files since last run',
self::PARTIAL => 'Scan specific directories only',
self::SELECTIVE => 'Scan based on custom criteria',
self::RECOVERY => 'Recovery scan after failure',
};
}
public function requiresCache(): bool
{
return match($this) {
self::INCREMENTAL, self::RECOVERY => true,
default => false,
};
}
public function isQuick(): bool
{
return match($this) {
self::PARTIAL, self::SELECTIVE => true,
default => false,
};
}
}

View File

@@ -0,0 +1,344 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Filesystem\FilePath;
use ArrayIterator;
use Countable;
use IteratorAggregate;
/**
* Immutable collection of TemplateMapping value objects
*
* Provides type-safe operations for template collections with filtering,
* searching, and memory-efficient operations.
*/
final readonly class TemplateCollection implements Countable, IteratorAggregate
{
/** @var array<TemplateMapping> */
private array $templates;
public function __construct(TemplateMapping ...$templates)
{
$this->templates = array_values($templates);
}
/**
* Add a template to the collection
*/
public function add(TemplateMapping $template): self
{
$templates = $this->templates;
$templates[] = $template;
return new self(...$templates);
}
/**
* Add multiple templates to the collection
*/
public function addMany(TemplateMapping ...$templates): self
{
return new self(...array_merge($this->templates, $templates));
}
/**
* Filter templates by name pattern
*/
public function filterByName(string $namePattern): self
{
$filtered = array_filter(
$this->templates,
fn (TemplateMapping $template) => $template->matchesName($namePattern)
);
return new self(...$filtered);
}
/**
* Filter templates by type
*/
public function filterByType(string $type): self
{
$filtered = array_filter(
$this->templates,
fn (TemplateMapping $template) => $template->isType($type)
);
return new self(...$filtered);
}
/**
* Filter templates by directory
*/
public function filterByDirectory(string $directory): self
{
$normalizedDirectory = rtrim($directory, '/');
$filtered = array_filter(
$this->templates,
fn (TemplateMapping $template) => dirname($template->path->toString()) === $normalizedDirectory
);
return new self(...$filtered);
}
/**
* Filter templates by extension
*/
public function filterByExtension(string $extension): self
{
$extension = ltrim($extension, '.');
$filtered = array_filter(
$this->templates,
fn (TemplateMapping $template) => $template->getExtension() === $extension
);
return new self(...$filtered);
}
/**
* Find template by exact name and type
*/
public function findExact(string $name, string $type = 'view'): ?TemplateMapping
{
foreach ($this->templates as $template) {
if ($template->name === $name && $template->type === $type) {
return $template;
}
}
return null;
}
/**
* Find template by name (any type)
*/
public function findByName(string $name): ?TemplateMapping
{
foreach ($this->templates as $template) {
if ($template->name === $name) {
return $template;
}
}
return null;
}
/**
* Find template by file path
*/
public function findByPath(FilePath $path): ?TemplateMapping
{
foreach ($this->templates as $template) {
if ($template->path->equals($path)) {
return $template;
}
}
return null;
}
/**
* Get templates grouped by type
*/
public function groupByType(): array
{
$grouped = [];
foreach ($this->templates as $template) {
if (! isset($grouped[$template->type])) {
$grouped[$template->type] = [];
}
$grouped[$template->type][] = $template;
}
return array_map(
fn (array $templates) => new self(...$templates),
$grouped
);
}
/**
* Get templates grouped by directory
*/
public function groupByDirectory(): array
{
$grouped = [];
foreach ($this->templates as $template) {
$directory = dirname($template->path->toString());
if (! isset($grouped[$directory])) {
$grouped[$directory] = [];
}
$grouped[$directory][] = $template;
}
return array_map(
fn (array $templates) => new self(...$templates),
$grouped
);
}
/**
* Get unique template names
*/
public function getUniqueNames(): array
{
$names = array_map(fn (TemplateMapping $template) => $template->name, $this->templates);
return array_unique($names);
}
/**
* Get unique template types
*/
public function getUniqueTypes(): array
{
$types = array_map(fn (TemplateMapping $template) => $template->type, $this->templates);
return array_unique($types);
}
/**
* Get unique directories
*/
public function getUniqueDirectories(): array
{
$directories = array_map(
fn (TemplateMapping $template) => dirname($template->path->toString()),
$this->templates
);
return array_unique($directories);
}
/**
* Remove duplicate templates based on unique identifier
*/
public function deduplicate(): self
{
$seen = [];
$unique = [];
foreach ($this->templates as $template) {
$key = $template->getUniqueId();
if (! isset($seen[$key])) {
$seen[$key] = true;
$unique[] = $template;
}
}
return new self(...$unique);
}
/**
* Sort templates by name
*/
public function sortedByName(): self
{
$sorted = $this->templates;
usort($sorted, fn (TemplateMapping $a, TemplateMapping $b) => $a->name <=> $b->name);
return new self(...$sorted);
}
/**
* Sort templates by type, then by name
*/
public function sortedByTypeAndName(): self
{
$sorted = $this->templates;
usort($sorted, function (TemplateMapping $a, TemplateMapping $b) {
$typeComparison = $a->type <=> $b->type;
return $typeComparison !== 0 ? $typeComparison : $a->name <=> $b->name;
});
return new self(...$sorted);
}
/**
* Check if collection is empty
*/
public function isEmpty(): bool
{
return empty($this->templates);
}
/**
* Get first template or null
*/
public function first(): ?TemplateMapping
{
return $this->templates[0] ?? null;
}
/**
* Get last template or null
*/
public function last(): ?TemplateMapping
{
return end($this->templates) ?: null;
}
/**
* Convert to array of TemplateMapping objects
*/
public function toArray(): array
{
return $this->templates;
}
/**
* Convert to legacy array format for backward compatibility
*/
public function toLegacyArray(): array
{
$legacy = [];
foreach ($this->templates as $template) {
// Group by template name with variants by type
if (! isset($legacy[$template->name])) {
$legacy[$template->name] = [];
}
$legacy[$template->name][$template->type] = $template->path->toString();
}
return $legacy;
}
/**
* Get memory footprint of entire collection
*/
public function getMemoryFootprint(): Byte
{
$totalBytes = 0;
foreach ($this->templates as $template) {
$totalBytes += $template->getMemoryFootprint()->toBytes();
}
// Add overhead for collection structure
$totalBytes += count($this->templates) * 8; // approximate pointer overhead
return Byte::fromBytes($totalBytes);
}
// Countable interface implementation
public function count(): int
{
return count($this->templates);
}
// IteratorAggregate interface implementation
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->templates);
}
}

View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Filesystem\FilePath;
/**
* Immutable value object for template mappings
* Replaces simple key-value arrays with memory-efficient typed structure
*/
final readonly class TemplateMapping
{
public function __construct(
public string $name,
public FilePath $path,
public string $type = 'view'
) {
}
public static function create(
string $name,
string $path,
string $type = 'view'
): self {
return new self(
name: $name,
path: FilePath::create($path),
type: $type
);
}
public static function fromPath(FilePath $path): self
{
$name = basename($path->toString(), '.php');
$type = str_contains($path->toString(), '/views/') ? 'view' : 'template';
return new self(
name: $name,
path: $path,
type: $type
);
}
/**
* Get unique identifier for deduplication
*/
public function getUniqueId(): string
{
return $this->name . '::' . $this->type;
}
/**
* Check if this mapping is the same as another
*/
public function isSameAs(self $other): bool
{
return $this->name === $other->name && $this->type === $other->type;
}
/**
* Check if template matches name pattern
*/
public function matchesName(string $pattern): bool
{
return fnmatch($pattern, $this->name);
}
/**
* Get template extension
*/
public function getExtension(): string
{
return pathinfo($this->path->toString(), PATHINFO_EXTENSION);
}
/**
* Check if template is of specific type
*/
public function isType(string $type): bool
{
return $this->type === $type;
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'name' => $this->name,
'path' => $this->path->toString(),
'type' => $this->type,
];
}
/**
* Create from array data
*/
public static function fromArray(array $data): self
{
// Handle both string paths and FilePath objects/arrays from old cache data
$path = $data['path'];
if (is_array($path)) {
// Old cache format: FilePath object was serialized as array
$path = $path['path'] ?? (string) reset($path);
} elseif (! is_string($path)) {
// Fallback: convert to string
$path = (string) $path;
}
// Skip invalid/empty paths from corrupted cache data
if (empty(trim($path))) {
throw new \InvalidArgumentException("Cannot create TemplateMapping with empty path");
}
return new self(
name: $data['name'],
path: FilePath::create($path),
type: $data['type'] ?? 'view'
);
}
/**
* Get memory footprint estimate
*/
public function getMemoryFootprint(): Byte
{
$bytes = strlen($this->name) +
strlen($this->path->toString()) +
strlen($this->type);
return Byte::fromBytes($bytes);
}
}