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; } }