Files
michaelschiemer/src/Framework/Cache/CachePattern.php
Michael Schiemer 55a330b223 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
2025-08-11 20:13:26 +02:00

213 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use InvalidArgumentException;
/**
* Cache pattern identifier for wildcard-based operations
* Supports patterns like "user:*", "cache.*.data", etc.
*/
final readonly class CachePattern implements CacheIdentifier
{
private const int MAX_PATTERN_LENGTH = 150;
private const string PATTERN_MARKER = 'pattern:';
private function __construct(
private string $pattern,
private string $compiledRegex
) {
$this->validate($pattern);
}
/**
* Create cache pattern from wildcard string
*
* Supports:
* - user:* (matches user:123, user:456, etc.)
* - cache.*.data (matches cache.sessions.data, cache.users.data)
* - temp:** (matches temp:anything:nested:deeply)
*/
public static function fromWildcard(string $pattern): self
{
$regex = self::compilePattern($pattern);
return new self($pattern, $regex);
}
/**
* Create pattern for all keys with prefix
*/
public static function withPrefix(string $prefix): self
{
return self::fromWildcard($prefix . '*');
}
/**
* Create pattern for all user-related keys
*/
public static function forUser(string|int $userId): self
{
return self::fromWildcard("user:{$userId}:*");
}
/**
* Create pattern for all session keys
*/
public static function forSessions(): self
{
return self::fromWildcard('session:*');
}
/**
* Create pattern for temporary keys
*/
public static function forTemporary(): self
{
return self::fromWildcard('temp:**');
}
/**
* Create pattern for namespace
*/
public static function forNamespace(string $namespace): self
{
return self::fromWildcard("{$namespace}:**");
}
public function toString(): string
{
return $this->pattern;
}
public function getType(): CacheIdentifierType
{
return CacheIdentifierType::PATTERN;
}
public function equals(CacheIdentifier $other): bool
{
return $other instanceof self && $this->pattern === $other->pattern;
}
public function matchesKey(CacheKey $key): bool
{
return preg_match($this->compiledRegex, $key->toString()) === 1;
}
public function getNormalizedString(): string
{
return self::PATTERN_MARKER . $this->pattern;
}
/**
* Get the original wildcard pattern
*/
public function getPattern(): string
{
return $this->pattern;
}
/**
* Get compiled regex pattern
*/
public function getCompiledRegex(): string
{
return $this->compiledRegex;
}
/**
* Check if pattern is simple prefix (ends with single *)
*/
public function isSimplePrefix(): bool
{
return str_ends_with($this->pattern, '*') &&
substr_count($this->pattern, '*') === 1 &&
! str_contains($this->pattern, '**');
}
/**
* Get prefix part for simple prefix patterns
*/
public function getPrefix(): ?string
{
if (! $this->isSimplePrefix()) {
return null;
}
return substr($this->pattern, 0, -1);
}
/**
* Check if pattern matches deep nesting (**)
*/
public function isDeepPattern(): bool
{
return str_contains($this->pattern, '**');
}
/**
* Estimate selectivity (0.0 = matches everything, 1.0 = very specific)
*/
public function getSelectivity(): float
{
$wildcardCount = substr_count($this->pattern, '*');
$deepCount = substr_count($this->pattern, '**');
$length = strlen($this->pattern);
// More specific patterns have higher selectivity
$specificity = $length / max(1, $wildcardCount * 5 + $deepCount * 10);
return min(1.0, $specificity / 20); // Normalize to 0-1 range
}
/**
* Compile wildcard pattern to regex
*/
private static function compilePattern(string $pattern): string
{
// Escape special regex characters except * and **
$escaped = preg_quote($pattern, '/');
// Replace escaped wildcards back
$escaped = str_replace('\\*\\*', '__DEEP_WILDCARD__', $escaped);
$escaped = str_replace('\\*', '__WILDCARD__', $escaped);
// Convert to regex
$regex = str_replace('__DEEP_WILDCARD__', '.*', $escaped);
$regex = str_replace('__WILDCARD__', '[^:]*', $regex);
return '/^' . $regex . '$/';
}
/**
* Validate the pattern
*/
private function validate(string $pattern): void
{
if (empty($pattern)) {
throw new InvalidArgumentException('Cache pattern cannot be empty');
}
if (strlen($pattern) > self::MAX_PATTERN_LENGTH) {
throw new InvalidArgumentException(sprintf(
'Cache pattern length exceeds maximum of %d characters (got %d)',
self::MAX_PATTERN_LENGTH,
strlen($pattern)
));
}
// Check for invalid characters
if (preg_match('/[\s\n\r\t\0\x0B]/', $pattern)) {
throw new InvalidArgumentException('Cache pattern contains invalid characters');
}
// Validate wildcard usage
if (str_contains($pattern, '***')) {
throw new InvalidArgumentException('Cache pattern cannot contain more than two consecutive wildcards');
}
}
}