132 lines
4.9 KiB
PHP
132 lines
4.9 KiB
PHP
<?php
|
|
|
|
namespace App\Serp\Providers\DataForSeo;
|
|
|
|
use App\Serp\Data\SerpResponse;
|
|
use App\Serp\Data\SerpResultData;
|
|
use App\Serp\Data\SerpSubmission;
|
|
use App\Serp\Data\SerpTaskResult;
|
|
use App\Serp\Exceptions\UnexpectedProviderResponseException;
|
|
|
|
final class DataForSeoResponseNormalizer
|
|
{
|
|
public function __construct(private DataForSeoErrorClassifier $errors) {}
|
|
|
|
public function live(array $data, int $limit): SerpResponse
|
|
{
|
|
$task = $this->task($data, [20000]);
|
|
|
|
return $this->response($task, $limit);
|
|
}
|
|
|
|
public function submission(array $data): SerpSubmission
|
|
{
|
|
$task = $this->task($data, [20000, 20100]);
|
|
$id = $task['id'] ?? null;
|
|
if (! is_string($id) || ! preg_match('/^[0-9a-f-]{36}$/i', $id)) {
|
|
throw new UnexpectedProviderResponseException('DataForSEO task id is missing.');
|
|
}
|
|
|
|
return new SerpSubmission('dataforseo', $id, $this->cost($task, $data), 'USD', (int) $task['status_code']);
|
|
}
|
|
|
|
public function collection(array $data, string $taskId, int $limit): SerpTaskResult
|
|
{
|
|
$this->envelope($data);
|
|
$tasks = $data['tasks'];
|
|
if ($tasks === [] || ! isset($tasks[0]) || ! is_array($tasks[0])) {
|
|
return SerpTaskResult::waiting();
|
|
}
|
|
$task = $tasks[0];
|
|
if (($task['id'] ?? null) !== $taskId) {
|
|
throw new UnexpectedProviderResponseException('DataForSEO task id mismatch.');
|
|
}
|
|
$status = $task['status_code'] ?? null;
|
|
if (! is_int($status)) {
|
|
throw new UnexpectedProviderResponseException('DataForSEO task status is missing.');
|
|
}
|
|
if (in_array($status, [20100, 40601, 40602], true) || ! array_key_exists('result', $task) || $task['result'] === null) {
|
|
return SerpTaskResult::waiting($status);
|
|
}
|
|
if ($status !== 20000) {
|
|
throw $this->errors->api($status);
|
|
}
|
|
$response = $this->response($task, $limit);
|
|
|
|
return SerpTaskResult::ready($response, $this->cost($task, $data), 'USD', $status);
|
|
}
|
|
|
|
private function envelope(array $data): void
|
|
{
|
|
$status = $data['status_code'] ?? null;
|
|
if (! is_int($status)) {
|
|
throw new UnexpectedProviderResponseException('DataForSEO envelope status is missing.');
|
|
}if ($status !== 20000) {
|
|
throw $this->errors->api($status);
|
|
}if (! isset($data['tasks']) || ! is_array($data['tasks'])) {
|
|
throw new UnexpectedProviderResponseException('DataForSEO tasks are missing.');
|
|
}
|
|
}
|
|
|
|
private function task(array $data, array $accepted): array
|
|
{
|
|
$this->envelope($data);
|
|
$task = $data['tasks'][0] ?? null;
|
|
if (! is_array($task) || ! is_int($task['status_code'] ?? null)) {
|
|
throw new UnexpectedProviderResponseException('DataForSEO task is malformed.');
|
|
}if (! in_array($task['status_code'], $accepted, true)) {
|
|
throw $this->errors->api($task['status_code']);
|
|
}
|
|
|
|
return $task;
|
|
}
|
|
|
|
private function response(array $task, int $limit): SerpResponse
|
|
{
|
|
$result = $task['result'] ?? null;
|
|
if (! is_array($result) || ! isset($result[0]) || ! is_array($result[0])) {
|
|
throw new UnexpectedProviderResponseException('DataForSEO result is missing.');
|
|
}
|
|
$items = $result[0]['items'] ?? null;
|
|
if (! is_array($items)) {
|
|
throw new UnexpectedProviderResponseException('DataForSEO items are missing.');
|
|
}
|
|
$rows = [];
|
|
$seen = [];
|
|
foreach ($items as $item) {
|
|
if (! is_array($item) || ($item['type'] ?? null) !== 'organic') {
|
|
continue;
|
|
}$position = $item['rank_group'] ?? null;
|
|
$url = $item['url'] ?? null;
|
|
if (! is_int($position) || $position < 1 || $position > $limit || ! is_string($url) || isset($seen[$position])) {
|
|
continue;
|
|
}try {
|
|
$rows[] = new SerpResultData($position, $url, $this->text($item['title'] ?? null, 500), $this->text($item['breadcrumb'] ?? null, 500), $this->text($item['description'] ?? null, 5000));
|
|
$seen[$position] = true;
|
|
} catch (\InvalidArgumentException) {
|
|
}
|
|
}
|
|
usort($rows, fn ($a, $b) => $a->position <=> $b->position);
|
|
|
|
return new SerpResponse('dataforseo', $rows, is_string($task['id'] ?? null) ? $task['id'] : null, $limit, $this->cost($task, []), 'USD', is_int($task['status_code'] ?? null) ? $task['status_code'] : null);
|
|
}
|
|
|
|
private function text(mixed $value, int $max): ?string
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}if (! is_string($value)) {
|
|
return null;
|
|
}
|
|
|
|
return mb_substr($value, 0, $max);
|
|
}
|
|
|
|
private function cost(array $task, array $data): ?float
|
|
{
|
|
$cost = $task['cost'] ?? $data['cost'] ?? null;
|
|
|
|
return is_int($cost) || is_float($cost) ? (float) $cost : null;
|
|
}
|
|
}
|