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'); } } }