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:
120
src/Framework/Discovery/ValueObjects/AttributeCollection.php
Normal file
120
src/Framework/Discovery/ValueObjects/AttributeCollection.php
Normal 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;
|
||||
}
|
||||
}
|
||||
13
src/Framework/Discovery/ValueObjects/AttributeTarget.php
Normal file
13
src/Framework/Discovery/ValueObjects/AttributeTarget.php
Normal 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';
|
||||
}
|
||||
103
src/Framework/Discovery/ValueObjects/CacheLevel.php
Normal file
103
src/Framework/Discovery/ValueObjects/CacheLevel.php
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
197
src/Framework/Discovery/ValueObjects/CacheMetrics.php
Normal file
197
src/Framework/Discovery/ValueObjects/CacheMetrics.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
184
src/Framework/Discovery/ValueObjects/CacheTier.php
Normal file
184
src/Framework/Discovery/ValueObjects/CacheTier.php
Normal 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
|
||||
}
|
||||
}
|
||||
116
src/Framework/Discovery/ValueObjects/CompressionLevel.php
Normal file
116
src/Framework/Discovery/ValueObjects/CompressionLevel.php
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
213
src/Framework/Discovery/ValueObjects/DiscoveredAttribute.php
Normal file
213
src/Framework/Discovery/ValueObjects/DiscoveredAttribute.php
Normal 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);
|
||||
}
|
||||
}
|
||||
248
src/Framework/Discovery/ValueObjects/DiscoveryConfiguration.php
Normal file
248
src/Framework/Discovery/ValueObjects/DiscoveryConfiguration.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
72
src/Framework/Discovery/ValueObjects/DiscoveryContext.php
Normal file
72
src/Framework/Discovery/ValueObjects/DiscoveryContext.php
Normal 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;
|
||||
}
|
||||
}
|
||||
70
src/Framework/Discovery/ValueObjects/DiscoveryOptions.php
Normal file
70
src/Framework/Discovery/ValueObjects/DiscoveryOptions.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
81
src/Framework/Discovery/ValueObjects/FileContext.php
Normal file
81
src/Framework/Discovery/ValueObjects/FileContext.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
72
src/Framework/Discovery/ValueObjects/InterfaceMapping.php
Normal file
72
src/Framework/Discovery/ValueObjects/InterfaceMapping.php
Normal 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);
|
||||
}
|
||||
}
|
||||
69
src/Framework/Discovery/ValueObjects/MemoryLeakInfo.php
Normal file
69
src/Framework/Discovery/ValueObjects/MemoryLeakInfo.php
Normal 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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
175
src/Framework/Discovery/ValueObjects/MemoryStrategy.php
Normal file
175
src/Framework/Discovery/ValueObjects/MemoryStrategy.php
Normal 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;
|
||||
}
|
||||
}
|
||||
22
src/Framework/Discovery/ValueObjects/ReflectionContext.php
Normal file
22
src/Framework/Discovery/ValueObjects/ReflectionContext.php
Normal 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
|
||||
) {
|
||||
}
|
||||
}
|
||||
79
src/Framework/Discovery/ValueObjects/ScanStrategy.php
Normal file
79
src/Framework/Discovery/ValueObjects/ScanStrategy.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
44
src/Framework/Discovery/ValueObjects/ScanType.php
Normal file
44
src/Framework/Discovery/ValueObjects/ScanType.php
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
344
src/Framework/Discovery/ValueObjects/TemplateCollection.php
Normal file
344
src/Framework/Discovery/ValueObjects/TemplateCollection.php
Normal 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);
|
||||
}
|
||||
}
|
||||
137
src/Framework/Discovery/ValueObjects/TemplateMapping.php
Normal file
137
src/Framework/Discovery/ValueObjects/TemplateMapping.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user