50) { throw new ValidationException('El ID del snippet no puede exceder 50 caracteres'); } // Validar formato generado (css_*) if (str_starts_with($id, self::PREFIX)) { if (!preg_match(self::PATTERN_GENERATED, $id)) { throw new ValidationException( sprintf('Formato de ID generado inválido: %s. Esperado: css_[timestamp]_[random]', $id) ); } return new self($id); } // Validar formato legacy (kebab-case) if (!preg_match(self::PATTERN_LEGACY, $id)) { throw new ValidationException( sprintf('Formato de ID inválido: %s. Use kebab-case (ej: cls-tables-apu)', $id) ); } return new self($id); } /** * Genera un nuevo SnippetId único * * @return self */ public static function generate(): self { $timestamp = time(); $random = bin2hex(random_bytes(3)); return new self(self::PREFIX . $timestamp . '_' . $random); } /** * Verifica si es un ID generado (vs legacy) * * @return bool */ public function isGenerated(): bool { return str_starts_with($this->value, self::PREFIX); } /** * Obtiene el valor del ID * * @return string */ public function value(): string { return $this->value; } /** * Compara igualdad con otro SnippetId * * @param SnippetId $other * @return bool */ public function equals(SnippetId $other): bool { return $this->value === $other->value; } /** * Representación string * * @return string */ public function __toString(): string { return $this->value; } }