94 lines
3.5 KiB
PHP
94 lines
3.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\DaData;
|
|
|
|
use Illuminate\Http\Client\ConnectionException;
|
|
use Illuminate\Http\Client\Factory as HttpFactory;
|
|
|
|
/**
|
|
* HTTP-обёртка над DaData clean/phone (spec §3.6).
|
|
*
|
|
* POST https://cleaner.dadata.ru/api/v1/clean/phone
|
|
* Authorization: Token <key> ; X-Secret: <secret> ; body ["<phone>"]
|
|
*
|
|
* Retry — только на сетевые ошибки и 5xx (4xx → сразу DaDataException, без retry).
|
|
* Сеть/таймаут после исчерпания retry → DaDataTimeoutException; 5xx → DaDataException.
|
|
* Конвенция клиента зеркалит App\Services\Supplier\SupplierPortalClient (inject HttpFactory).
|
|
*/
|
|
class DaDataPhoneClient
|
|
{
|
|
private const URL = 'https://cleaner.dadata.ru/api/v1/clean/phone';
|
|
|
|
public function __construct(
|
|
private readonly HttpFactory $http,
|
|
) {}
|
|
|
|
public function cleanPhone(string $phone): DaDataPhoneResponse
|
|
{
|
|
$cfg = (array) config('services.dadata');
|
|
$timeoutSec = max(1, (int) round(((int) ($cfg['timeout_ms'] ?? 2000)) / 1000));
|
|
$attempts = max(1, (int) ($cfg['retries'] ?? 1) + 1);
|
|
$apiKey = (string) ($cfg['api_key'] ?? '');
|
|
$secret = (string) ($cfg['secret'] ?? '');
|
|
|
|
$lastException = null;
|
|
|
|
for ($attempt = 0; $attempt < $attempts; $attempt++) {
|
|
try {
|
|
$response = $this->http
|
|
->asJson()
|
|
->acceptJson()
|
|
->timeout($timeoutSec)
|
|
->withHeaders([
|
|
'Authorization' => 'Token '.$apiKey,
|
|
'X-Secret' => $secret,
|
|
])
|
|
->post(self::URL, [$phone]);
|
|
} catch (ConnectionException $e) {
|
|
$lastException = new DaDataTimeoutException(
|
|
'DaData connection failed: '.$e->getMessage(), 0, $e,
|
|
);
|
|
|
|
continue; // сеть → retry
|
|
}
|
|
|
|
if ($response->serverError()) {
|
|
$lastException = new DaDataException('DaData 5xx: HTTP '.$response->status());
|
|
|
|
continue; // 5xx → retry
|
|
}
|
|
|
|
if (! $response->successful()) {
|
|
// 4xx — клиентская ошибка, retry бессмыслен.
|
|
throw new DaDataException('DaData HTTP '.$response->status().': '.$response->body());
|
|
}
|
|
|
|
return $this->parse($response->json());
|
|
}
|
|
|
|
throw $lastException ?? new DaDataException('DaData failed without a response');
|
|
}
|
|
|
|
/**
|
|
* @param mixed $body декодированный JSON (ожидается массив с одним объектом)
|
|
*/
|
|
private function parse($body): DaDataPhoneResponse
|
|
{
|
|
$row = (is_array($body) && isset($body[0]) && is_array($body[0])) ? $body[0] : [];
|
|
|
|
return new DaDataPhoneResponse(
|
|
qc: isset($row['qc']) ? (int) $row['qc'] : null,
|
|
qcConflict: isset($row['qc_conflict']) ? (int) $row['qc_conflict'] : null,
|
|
type: isset($row['type']) ? (string) $row['type'] : null,
|
|
phone: isset($row['phone']) ? (string) $row['phone'] : null,
|
|
provider: isset($row['provider']) ? (string) $row['provider'] : null,
|
|
region: isset($row['region']) ? (string) $row['region'] : null,
|
|
city: isset($row['city']) ? (string) $row['city'] : null,
|
|
timezone: isset($row['timezone']) ? (string) $row['timezone'] : null,
|
|
raw: $row,
|
|
);
|
|
}
|
|
}
|