104 lines
4.5 KiB
PHP
104 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace App\Sms\Provider;
|
|
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
|
|
class KavehNegarProvider implements SmsProviderInterface
|
|
{
|
|
private const BASE = 'https://api.kavenegar.com/v1';
|
|
|
|
public function __construct(
|
|
private readonly HttpClientInterface $httpClient,
|
|
private readonly LoggerInterface $logger,
|
|
// API key comes only from env `%env(default::KAVENEGAR_API_KEY)%`; null when unset.
|
|
private readonly ?string $apiKey = null,
|
|
private readonly ?string $sender = null,
|
|
) {}
|
|
|
|
private function key(): string { return $this->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;
|
|
}
|
|
}
|
|
}
|