request()->get('/hardware', [ 'limit' => $pageSize, 'offset' => max(0, $offset), 'sort' => 'id', 'order' => 'asc', ]); if (! $response->successful()) { throw new SnipeItApiException( "O Snipe-IT respondeu com HTTP {$response->status()} ao consultar equipamentos.", $response->status(), ); } $payload = $response->json(); if (! is_array($payload)) { throw new SnipeItApiException('O Snipe-IT devolveu uma resposta que não é JSON válido.'); } if (($payload['status'] ?? null) === 'error') { throw new SnipeItApiException('O Snipe-IT recusou a consulta de equipamentos apesar de responder com HTTP 200.'); } $total = $payload['total'] ?? null; $rows = $payload['rows'] ?? null; if (! is_numeric($total) || ! is_array($rows)) { throw new SnipeItApiException('A resposta do Snipe-IT não contém os campos de paginação esperados.'); } /** @var list> $normalizedRows */ $normalizedRows = []; foreach (array_values($rows) as $row) { if (! is_array($row)) { throw new SnipeItApiException('A resposta do Snipe-IT contém um equipamento num formato inválido.'); } /** @var array $row */ $normalizedRows[] = $row; } return new HardwarePage((int) $total, max(0, $offset), $pageSize, $normalizedRows); } /** @return Generator */ public function hardwarePages(): Generator { $offset = 0; do { $page = $this->listHardwarePage($offset); yield $page; $received = count($page->rows); $offset += $received; } while ($received > 0 && $offset < $page->total); } private function request(): PendingRequest { if (! $this->isConfigured()) { throw new SnipeItConfigurationException('A URL e o token de leitura do Snipe-IT ainda não estão configurados.'); } $retries = max(1, (int) config('services.snipe_it.retries', 3)); $retryDelay = max(0, (int) config('services.snipe_it.retry_delay_ms', 250)); return Http::baseUrl($this->apiBaseUrl()) ->acceptJson() ->asJson() ->withToken((string) config('services.snipe_it.read_token')) ->withUserAgent(Str::slug((string) config('app.name')).'/inventory-sync') ->connectTimeout(min(5, max(1, (int) config('services.snipe_it.timeout', 10)))) ->timeout(max(1, (int) config('services.snipe_it.timeout', 10))) ->retry( $retries, $retryDelay, fn (Throwable $exception): bool => $this->shouldRetry($exception), throw: false, ); } private function apiBaseUrl(): string { $baseUrl = $this->configuredUrl(); if ($baseUrl === null || ! filter_var($baseUrl, FILTER_VALIDATE_URL)) { throw new SnipeItConfigurationException('A URL configurada para o Snipe-IT não é válida.'); } return str_ends_with($baseUrl, '/api/v1') ? $baseUrl : $baseUrl.'/api/v1'; } private function shouldRetry(Throwable $exception): bool { if ($exception instanceof ConnectionException) { return true; } if (! $exception instanceof RequestException || $exception->response === null) { return false; } return $exception->response->status() === 429 || $exception->response->serverError(); } }