apiKey ?? ''; } private function sender(): string { return $this->sender ?? ''; } public function getName(): string { return 'kavenegar'; } // Kavenegar فقط GET با query را می‌پذیرد (مطابق مستند رسمی). هیچ درخواست POST/body نباید ساخته شود. // GET بدنه ندارد پس curl هدر `Expect: 100-continue` نمی‌فرستد (رفع idle timeout) و حجم درخواست کوچک می‌ماند. private const MAX_ATTEMPTS = 3; /** یک GET به کاوه‌نگار با query params و retry برای خطاهای گذرا؛ آرایهٔ پاسخ را برمی‌گرداند. */ private function get(string $path, array $query): array { $url = self::BASE . '/' . $this->key() . $path; for ($attempt = 1; ; $attempt++) { try { return $this->httpClient->request('GET', $url, [ 'query' => $query, 'timeout' => 15, 'max_duration' => 30, 'proxy' => null, ])->toArray(); } catch (TransportExceptionInterface $e) { if ($attempt >= self::MAX_ATTEMPTS) { throw $e; } usleep(500_000 * $attempt); } } } public function send(string $mobile, string $message): bool { try { $data = $this->get('/sms/send.json', [ 'receptor' => $mobile, 'message' => $message, 'sender' => $this->sender(), ]); return ($data['return']['status'] ?? 0) === 200; } catch (\Throwable $e) { $this->logger->error(sprintf('SMS send failed (kavenegar): %s @ %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()), ['exception' => $e, 'mobile' => $mobile]); return false; } } // token/token2/token3 مقدارِ دارای فاصله را رد می‌کنند؛ فاصله را با نیم‌فاصله (ZWNJ) جایگزین می‌کنیم. private const NO_SPACE_SLOTS = ['token', 'token2', 'token3']; private function forSlot(string $slot, string $value): string { if (!in_array($slot, self::NO_SPACE_SLOTS, true)) { return $value; } return preg_replace('/\s+/u', "\u{200C}", $value) ?? $value; } public function sendTemplate(string $mobile, string $templateCode, array $vars): bool { try { $params = ['receptor' => $mobile, 'template' => $templateCode]; // Kavenegar token slots: token/token2/token3 reject spaces; token10/token20 allow them. // If every key is an explicit slot name, honor it (e.g. site with spaces → token10); // otherwise fall back to positional mapping (token, token2, token3, ...). $slots = ['token', 'token2', 'token3', 'token10', 'token20']; if ($vars !== [] && array_keys($vars) !== range(0, count($vars) - 1) && array_diff(array_keys($vars), $slots) === []) { foreach ($vars as $slot => $v) { $params[$slot] = $this->forSlot($slot, (string) $v); } } else { foreach (array_values($vars) as $i => $v) { $slot = 'token' . ($i > 0 ? $i + 1 : ''); $params[$slot] = $this->forSlot($slot, (string) $v); } } $data = $this->get('/verify/lookup.json', $params); return ($data['return']['status'] ?? 0) === 200; } catch (\Throwable $e) { $this->logger->error(sprintf('SMS sendTemplate failed (kavenegar): %s @ %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()), ['exception' => $e, 'mobile' => $mobile, 'template' => $templateCode]); return false; } } }