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,46 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow\Aggregator;
use App\Framework\SlidingWindow\WindowEntry;
/**
* Aggregator for boolean values (success/failure) - useful for CircuitBreaker
*
* @implements SlidingWindowAggregator<BooleanResult>
*/
final readonly class BooleanAggregator implements SlidingWindowAggregator
{
public function aggregate(WindowEntry ...$entries): BooleanResult
{
$successes = 0;
$failures = 0;
foreach ($entries as $entry) {
if ($entry->value === true) {
$successes++;
} elseif ($entry->value === false) {
$failures++;
}
}
$total = $successes + $failures;
$failureRate = $total > 0 ? $failures / $total : 0.0;
$successRate = $total > 0 ? $successes / $total : 0.0;
return new BooleanResult(
total: $total,
successes: $successes,
failures: $failures,
successRate: $successRate,
failureRate: $failureRate
);
}
public function getIdentifier(): string
{
return 'boolean';
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow\Aggregator;
/**
* Result of boolean aggregation
*/
final readonly class BooleanResult
{
public function __construct(
public int $total,
public int $successes,
public int $failures,
public float $successRate,
public float $failureRate,
) {
}
public function isEmpty(): bool
{
return $this->total === 0;
}
public function hasMinimumRequests(int $minimum): bool
{
return $this->total >= $minimum;
}
public function isFailureRateExceeded(float $threshold): bool
{
return $this->failureRate > $threshold;
}
public function isSuccessRateBelow(float $threshold): bool
{
return $this->successRate < $threshold;
}
public function toArray(): array
{
return [
'total' => $this->total,
'successes' => $this->successes,
'failures' => $this->failures,
'success_rate' => $this->successRate,
'failure_rate' => $this->failureRate,
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow\Aggregator;
use App\Framework\SlidingWindow\WindowEntry;
/**
* Aggregator for counting entries - useful for RateLimiter
*
* @implements SlidingWindowAggregator<CountingResult>
*/
final readonly class CountingAggregator implements SlidingWindowAggregator
{
public function aggregate(WindowEntry ...$entries): CountingResult
{
return new CountingResult(
count: count($entries),
values: array_map(fn (WindowEntry $entry) => $entry->value, $entries)
);
}
public function getIdentifier(): string
{
return 'counting';
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow\Aggregator;
/**
* Result of counting aggregation
*/
final readonly class CountingResult
{
public function __construct(
public int $count,
public array $values = [],
) {
}
public function isEmpty(): bool
{
return $this->count === 0;
}
public function hasMinimum(int $minimum): bool
{
return $this->count >= $minimum;
}
public function toArray(): array
{
return [
'count' => $this->count,
'values' => $this->values,
];
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow\Aggregator;
use App\Framework\SlidingWindow\WindowEntry;
/**
* Aggregator for numeric values - useful for Performance Metrics
*
* @implements SlidingWindowAggregator<NumericResult>
*/
final readonly class NumericAggregator implements SlidingWindowAggregator
{
public function aggregate(WindowEntry ...$entries): NumericResult
{
if (empty($entries)) {
return new NumericResult(
count: 0,
sum: 0,
average: 0,
min: null,
max: null,
median: null,
percentile95: null
);
}
$values = array_map(fn (WindowEntry $entry) => (float) $entry->value, $entries);
sort($values);
$count = count($values);
$sum = array_sum($values);
$average = $sum / $count;
$min = min($values);
$max = max($values);
// Calculate median
$middle = intval($count / 2);
$median = $count % 2 === 0
? ($values[$middle - 1] + $values[$middle]) / 2
: $values[$middle];
// Calculate 95th percentile
$p95Index = intval($count * 0.95);
$p95Index = min($p95Index, $count - 1);
$percentile95 = $values[$p95Index];
return new NumericResult(
count: $count,
sum: $sum,
average: $average,
min: $min,
max: $max,
median: $median,
percentile95: $percentile95
);
}
public function getIdentifier(): string
{
return 'numeric';
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow\Aggregator;
/**
* Result of numeric aggregation
*/
final readonly class NumericResult
{
public function __construct(
public int $count,
public float $sum,
public float $average,
public ?float $min,
public ?float $max,
public ?float $median,
public ?float $percentile95,
) {
}
public function isEmpty(): bool
{
return $this->count === 0;
}
public function hasMinimumSamples(int $minimum): bool
{
return $this->count >= $minimum;
}
public function isAverageAbove(float $threshold): bool
{
return $this->average > $threshold;
}
public function isPercentile95Above(float $threshold): bool
{
return $this->percentile95 !== null && $this->percentile95 > $threshold;
}
public function toArray(): array
{
return [
'count' => $this->count,
'sum' => $this->sum,
'average' => $this->average,
'min' => $this->min,
'max' => $this->max,
'median' => $this->median,
'percentile_95' => $this->percentile95,
];
}
}

View File

@@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow\Aggregator;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\SlidingWindow\WindowEntry;
/**
* Advanced aggregator for rate limiting with burst detection and threat analysis
*
* @implements SlidingWindowAggregator<RateLimitResult>
*/
final readonly class RateLimitAggregator implements SlidingWindowAggregator
{
public function __construct(
private float $burstThreshold = 0.8,
private float $minIntervalThreshold = 2.0
) {
}
public function aggregate(WindowEntry ...$entries): RateLimitResult
{
$timestamps = array_map(fn (WindowEntry $entry) => $entry->timestamp, $entries);
$count = count($entries);
if ($count < 2) {
return new RateLimitResult(
count: $count,
timestamps: $timestamps
);
}
// Sort timestamps for analysis
$sortedTimestamps = $timestamps;
usort($sortedTimestamps, fn (Timestamp $a, Timestamp $b) => $a->toFloat() <=> $b->toFloat());
// Calculate intervals between requests
$intervals = [];
for ($i = 1; $i < count($sortedTimestamps); $i++) {
$intervals[] = $sortedTimestamps[$i]->toFloat() - $sortedTimestamps[$i - 1]->toFloat();
}
// Burst analysis
$avgInterval = array_sum($intervals) / count($intervals);
$minInterval = min($intervals);
// Calculate burst intensity (lower intervals = higher intensity)
$burstIntensity = $avgInterval > 0 ? ($avgInterval - $minInterval) / $avgInterval : 1.0;
$burstDetected = $burstIntensity > $this->burstThreshold && $minInterval < $this->minIntervalThreshold;
// Threat analysis based on patterns
$threatAnalysis = $this->analyzeThreatPatterns($intervals, $count, $burstIntensity);
return new RateLimitResult(
count: $count,
timestamps: $timestamps,
burstIntensity: $burstIntensity,
minInterval: $minInterval,
avgInterval: $avgInterval,
burstDetected: $burstDetected,
threatAnalysis: $threatAnalysis
);
}
public function getIdentifier(): string
{
return 'rate_limit';
}
/**
* Analyze threat patterns from request intervals
*/
private function analyzeThreatPatterns(array $intervals, int $count, float $burstIntensity): array
{
$patterns = [];
// High frequency attack
if ($count > 100) {
$patterns[] = 'high_frequency_attack';
}
// Burst attack pattern
if ($burstIntensity > $this->burstThreshold) {
$patterns[] = 'burst_attack';
}
// Sustained attack pattern (many requests with consistent timing)
$intervalVariance = $this->calculateVariance($intervals);
if ($intervalVariance < 0.1 && count($intervals) > 10) {
$patterns[] = 'sustained_attack';
}
// Gradual ramp-up attack
if ($this->detectRampUpPattern($intervals)) {
$patterns[] = 'ramp_up_attack';
}
return [
'patterns' => $patterns,
'confidence' => $this->calculateConfidence($patterns, $count, $burstIntensity),
'threat_score' => $this->calculateThreatScore($patterns, $count, $burstIntensity),
'interval_variance' => $intervalVariance ?? 0.0,
];
}
/**
* Calculate variance of intervals
*/
private function calculateVariance(array $intervals): float
{
if (count($intervals) < 2) {
return 0.0;
}
$mean = array_sum($intervals) / count($intervals);
$variance = array_sum(array_map(fn ($x) => pow($x - $mean, 2), $intervals)) / count($intervals);
return $variance;
}
/**
* Detect ramp-up attack pattern (increasing frequency over time)
*/
private function detectRampUpPattern(array $intervals): bool
{
if (count($intervals) < 5) {
return false;
}
// Check if intervals are generally decreasing (frequency increasing)
$decreasing = 0;
for ($i = 1; $i < count($intervals); $i++) {
if ($intervals[$i] < $intervals[$i - 1]) {
$decreasing++;
}
}
// If more than 70% of intervals are decreasing, it's likely a ramp-up
return ($decreasing / (count($intervals) - 1)) > 0.7;
}
/**
* Calculate confidence score for threat detection
*/
private function calculateConfidence(array $patterns, int $count, float $burstIntensity): float
{
$confidence = 0.0;
// Base confidence on number of patterns detected
$confidence += count($patterns) * 0.2;
// High request count increases confidence
if ($count > 50) {
$confidence += 0.3;
}
// High burst intensity increases confidence
if ($burstIntensity > 0.8) {
$confidence += 0.4;
}
return min(1.0, $confidence);
}
/**
* Calculate threat score (0-100)
*/
private function calculateThreatScore(array $patterns, int $count, float $burstIntensity): float
{
$score = 0.0;
// Pattern-based scoring
$patternScores = [
'high_frequency_attack' => 30,
'burst_attack' => 25,
'sustained_attack' => 20,
'ramp_up_attack' => 15,
];
foreach ($patterns as $pattern) {
$score += $patternScores[$pattern] ?? 0;
}
// Count-based scoring
if ($count > 100) {
$score += 20;
} elseif ($count > 50) {
$score += 10;
}
// Burst intensity scoring
$score += $burstIntensity * 25;
return min(100.0, $score);
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow\Aggregator;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Result object for rate limit aggregations with threat analysis
*/
final readonly class RateLimitResult
{
public function __construct(
public int $count,
public array $timestamps,
public ?float $burstIntensity = null,
public ?float $minInterval = null,
public ?float $avgInterval = null,
public bool $burstDetected = false,
public array $threatAnalysis = [],
) {
}
/**
* Get all timestamps as array of floats
*/
public function getTimestampsAsFloats(): array
{
return array_map(
fn (Timestamp $timestamp) => $timestamp->toFloat(),
$this->timestamps
);
}
/**
* Check if burst pattern is detected
*/
public function hasBurstPattern(): bool
{
return $this->burstDetected;
}
/**
* Get burst analysis data
*/
public function getBurstAnalysis(): array
{
return [
'burst_detected' => $this->burstDetected,
'burst_intensity' => $this->burstIntensity,
'min_interval' => $this->minInterval,
'avg_interval' => $this->avgInterval,
'request_count' => $this->count,
];
}
/**
* Convert to array for storage/serialization
*/
public function toArray(): array
{
return [
'count' => $this->count,
'timestamps' => $this->getTimestampsAsFloats(),
'burst_analysis' => $this->getBurstAnalysis(),
'threat_analysis' => $this->threatAnalysis,
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow\Aggregator;
use App\Framework\SlidingWindow\WindowEntry;
/**
* Interface for aggregating sliding window data
*
* @template TResult
*/
interface SlidingWindowAggregator
{
/**
* Aggregate window entries into summary data
*
* @return TResult
*/
public function aggregate(WindowEntry ...$entries): mixed;
/**
* Get aggregator identifier
*/
public function getIdentifier(): string;
}

View File

@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\SlidingWindow\Aggregator\SlidingWindowAggregator;
/**
* Cache-based sliding window implementation for persistence
*/
final readonly class CacheBasedSlidingWindow implements SlidingWindow
{
private const string CACHE_PREFIX = 'sliding_window:';
public function __construct(
private Cache $cache,
private Duration $windowSize,
private string $identifier,
private SlidingWindowAggregator $aggregator,
private ?Duration $cacheTtl = null,
) {
$this->cacheTtl ??= Duration::fromHours(1);
}
public function record(mixed $value, Timestamp $timestamp): void
{
$entries = $this->getEntriesFromCache();
$entries[] = new WindowEntry($value, $timestamp);
$this->storeEntriesToCache($entries);
$this->cleanup();
}
public function getValues(?Timestamp $now = null): array
{
$now ??= Timestamp::now();
$this->cleanup($now);
return array_map(
fn (WindowEntry $entry) => $entry->value,
$this->getValidEntries($now)
);
}
public function getStats(?Timestamp $now = null): SlidingWindowStats
{
$now ??= Timestamp::now();
$this->cleanup($now);
$validEntries = $this->getValidEntries($now);
$windowStart = $now->diff($this->windowSize) > Duration::zero()
? Timestamp::fromFloat($now->toFloat() - $this->windowSize->toSeconds())
: $now;
$aggregatedData = [];
if (! empty($validEntries)) {
$result = $this->aggregator->aggregate(...$validEntries);
$aggregatedData = method_exists($result, 'toArray') ? $result->toArray() : [$this->aggregator->getIdentifier() => $result];
}
return new SlidingWindowStats(
totalCount: count($validEntries),
windowSize: $this->windowSize,
windowStart: $windowStart,
windowEnd: $now,
aggregatedData: $aggregatedData
);
}
public function clear(): void
{
$cacheKey = $this->getCacheKey();
$this->cache->forget($cacheKey);
}
public function getWindowSize(): Duration
{
return $this->windowSize;
}
public function getIdentifier(): string
{
return $this->identifier;
}
/**
* @return array<WindowEntry>
*/
private function getEntriesFromCache(): array
{
$cacheKey = $this->getCacheKey();
$cacheItem = $this->cache->get($cacheKey);
if (! $cacheItem->isHit) {
return [];
}
$serializedEntries = $cacheItem->value;
if (! is_array($serializedEntries)) {
return [];
}
return array_map(
fn (array $data) => new WindowEntry(
$data['value'],
Timestamp::fromFloat($data['timestamp'])
),
$serializedEntries
);
}
/**
* @param array<WindowEntry> $entries
*/
private function storeEntriesToCache(array $entries): void
{
$cacheKey = $this->getCacheKey();
$serializedEntries = array_map(
fn (WindowEntry $entry) => [
'value' => $entry->value,
'timestamp' => $entry->timestamp->toFloat(),
],
$entries
);
$this->cache->set($cacheKey, $serializedEntries, $this->cacheTtl);
}
/**
* @return array<WindowEntry>
*/
private function getValidEntries(Timestamp $now): array
{
$entries = $this->getEntriesFromCache();
$cutoffTime = Timestamp::fromFloat($now->toFloat() - $this->windowSize->toSeconds());
return array_filter(
$entries,
fn (WindowEntry $entry) => $entry->isWithinWindow($cutoffTime)
);
}
private function cleanup(?Timestamp $now = null): void
{
$now ??= Timestamp::now();
$validEntries = $this->getValidEntries($now);
$this->storeEntriesToCache($validEntries);
}
private function getCacheKey(): CacheKey
{
return CacheKey::fromString(self::CACHE_PREFIX . $this->identifier);
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\SlidingWindow\Aggregator\SlidingWindowAggregator;
/**
* In-memory sliding window implementation
*/
final class InMemorySlidingWindow implements SlidingWindow
{
/**
* @var array<WindowEntry>
*/
private array $entries = [];
public function __construct(
private readonly Duration $windowSize,
private readonly string $identifier,
private readonly SlidingWindowAggregator $aggregator,
) {
}
public function record(mixed $value, Timestamp $timestamp): void
{
$this->entries[] = new WindowEntry($value, $timestamp);
$this->cleanup();
}
public function getValues(?Timestamp $now = null): array
{
$now ??= Timestamp::now();
$this->cleanup($now);
return array_map(
fn (WindowEntry $entry) => $entry->value,
$this->getValidEntries($now)
);
}
public function getStats(?Timestamp $now = null): SlidingWindowStats
{
$now ??= Timestamp::now();
$this->cleanup($now);
$validEntries = $this->getValidEntries($now);
$windowStart = $now->diff($this->windowSize) > Duration::zero()
? Timestamp::fromFloat($now->toFloat() - $this->windowSize->toSeconds())
: $now;
$aggregatedData = [];
if (! empty($validEntries)) {
$result = $this->aggregator->aggregate(...$validEntries);
$aggregatedData = method_exists($result, 'toArray') ? $result->toArray() : [$this->aggregator->getIdentifier() => $result];
}
return new SlidingWindowStats(
totalCount: count($validEntries),
windowSize: $this->windowSize,
windowStart: $windowStart,
windowEnd: $now,
aggregatedData: $aggregatedData
);
}
public function clear(): void
{
$this->entries = [];
}
public function getWindowSize(): Duration
{
return $this->windowSize;
}
public function getIdentifier(): string
{
return $this->identifier;
}
/**
* Get all valid entries within the window
*
* @return array<WindowEntry>
*/
private function getValidEntries(Timestamp $now): array
{
$cutoffTime = Timestamp::fromFloat($now->toFloat() - $this->windowSize->toSeconds());
return array_filter(
$this->entries,
fn (WindowEntry $entry) => $entry->isWithinWindow($cutoffTime)
);
}
/**
* Remove entries outside the window
*/
private function cleanup(?Timestamp $now = null): void
{
$now ??= Timestamp::now();
$this->entries = $this->getValidEntries($now);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Generic sliding window interface for time-based data collection
*
* @template T
*/
interface SlidingWindow
{
/**
* Record a value with timestamp
*
* @param mixed $value
*/
public function record(mixed $value, Timestamp $timestamp): void;
/**
* Get all values within the window
*
* @return array<mixed>
*/
public function getValues(?Timestamp $now = null): array;
/**
* Get aggregated statistics for the window
*/
public function getStats(?Timestamp $now = null): SlidingWindowStats;
/**
* Clear all data from the window
*/
public function clear(): void;
/**
* Get window configuration
*/
public function getWindowSize(): Duration;
/**
* Get window identifier
*/
public function getIdentifier(): string;
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow;
use App\Framework\Cache\Cache;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\SlidingWindow\Aggregator\BooleanAggregator;
use App\Framework\SlidingWindow\Aggregator\CountingAggregator;
use App\Framework\SlidingWindow\Aggregator\NumericAggregator;
use App\Framework\SlidingWindow\Aggregator\RateLimitAggregator;
use App\Framework\SlidingWindow\Aggregator\SlidingWindowAggregator;
/**
* Factory for creating sliding windows
*/
final readonly class SlidingWindowFactory
{
public function __construct(
private ?Cache $cache = null,
) {
}
/**
* Create a counting sliding window (for rate limiting)
*/
public function createCountingWindow(
string $identifier,
Duration $windowSize,
bool $persistent = false
): SlidingWindow {
$aggregator = new CountingAggregator();
return $persistent && $this->cache !== null
? new CacheBasedSlidingWindow($this->cache, $windowSize, $identifier, $aggregator)
: new InMemorySlidingWindow($windowSize, $identifier, $aggregator);
}
/**
* Create a boolean sliding window (for circuit breakers)
*/
public function createBooleanWindow(
string $identifier,
Duration $windowSize,
bool $persistent = false
): SlidingWindow {
$aggregator = new BooleanAggregator();
return $persistent && $this->cache !== null
? new CacheBasedSlidingWindow($this->cache, $windowSize, $identifier, $aggregator)
: new InMemorySlidingWindow($windowSize, $identifier, $aggregator);
}
/**
* Create a numeric sliding window (for performance metrics)
*/
public function createNumericWindow(
string $identifier,
Duration $windowSize,
bool $persistent = false
): SlidingWindow {
$aggregator = new NumericAggregator();
return $persistent && $this->cache !== null
? new CacheBasedSlidingWindow($this->cache, $windowSize, $identifier, $aggregator)
: new InMemorySlidingWindow($windowSize, $identifier, $aggregator);
}
/**
* Create a rate limit sliding window with advanced analytics
*/
public function createRateLimitWindow(
string $identifier,
Duration $windowSize,
bool $persistent = false,
float $burstThreshold = 0.8,
float $minIntervalThreshold = 2.0
): SlidingWindow {
$aggregator = new RateLimitAggregator($burstThreshold, $minIntervalThreshold);
return $persistent && $this->cache !== null
? new CacheBasedSlidingWindow($this->cache, $windowSize, $identifier, $aggregator)
: new InMemorySlidingWindow($windowSize, $identifier, $aggregator);
}
/**
* Create a custom sliding window with provided aggregator
*/
public function createCustomWindow(
string $identifier,
Duration $windowSize,
SlidingWindowAggregator $aggregator,
bool $persistent = false
): SlidingWindow {
return $persistent && $this->cache !== null
? new CacheBasedSlidingWindow($this->cache, $windowSize, $identifier, $aggregator)
: new InMemorySlidingWindow($windowSize, $identifier, $aggregator);
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Statistics aggregated from a sliding window
*/
final readonly class SlidingWindowStats
{
public function __construct(
public int $totalCount,
public Duration $windowSize,
public Timestamp $windowStart,
public Timestamp $windowEnd,
public array $aggregatedData = [],
) {
}
/**
* Check if minimum count threshold is met
*/
public function hasMinimumCount(int $minimum): bool
{
return $this->totalCount >= $minimum;
}
/**
* Get rate per second
*/
public function getRatePerSecond(): float
{
$seconds = $this->windowSize->toSeconds();
return $seconds > 0 ? $this->totalCount / $seconds : 0.0;
}
/**
* Get rate per minute
*/
public function getRatePerMinute(): float
{
return $this->getRatePerSecond() * 60;
}
/**
* Get aggregated value by key
*/
public function getAggregatedValue(string $key): mixed
{
return $this->aggregatedData[$key] ?? null;
}
/**
* Check if window is empty
*/
public function isEmpty(): bool
{
return $this->totalCount === 0;
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'total_count' => $this->totalCount,
'window_size' => $this->windowSize->toHumanReadable(),
'window_start' => $this->windowStart->toIsoString(),
'window_end' => $this->windowEnd->toIsoString(),
'rate_per_second' => $this->getRatePerSecond(),
'rate_per_minute' => $this->getRatePerMinute(),
'aggregated_data' => $this->aggregatedData,
];
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Framework\SlidingWindow;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Single entry in a sliding window
*/
final readonly class WindowEntry
{
public function __construct(
public mixed $value,
public Timestamp $timestamp,
) {
}
/**
* Check if entry is within window from given timestamp
*/
public function isWithinWindow(Timestamp $fromTimestamp): bool
{
return $this->timestamp->isAfter($fromTimestamp) || $this->timestamp->equals($fromTimestamp);
}
/**
* Convert to array for serialization
*/
public function toArray(): array
{
return [
'value' => $this->value,
'timestamp' => $this->timestamp->toIsoString(),
];
}
}