- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
213 lines
5.4 KiB
PHP
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');
|
|
}
|
|
}
|
|
}
|