Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
ApiClient
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
2 / 2
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 request
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
9
1<?php
2
3declare(strict_types=1);
4
5namespace App\Shared\Api;
6
7use App\Domain\Api\Repository\ApiRecordRepository;
8use App\Domain\Host\Repository\HostRecordRepository;
9
10 class ApiClient implements ApiClientInterface
11{
12    public function __construct(
13        private HostRecordRepository $hostRepository,
14        private ApiRecordRepository $apiRepository
15    ) {}
16
17    public function request(string $hostId, string $method, string $path, array $queryParams = [], ?array $body = null): array
18    {
19        $host = $this->hostRepository->findById($hostId);
20        $apiRecord = $this->apiRepository->findByHostId($hostId);
21
22        if (!$host || !$apiRecord) {
23            throw new \InvalidArgumentException("Host or API configuration not found for host ID: $hostId");
24        }
25
26        $url = rtrim($apiRecord->getEndpointUrl() ?: $host->getUrl(), '/') . '/' . ltrim($path, '/');
27
28        if (!empty($queryParams)) {
29            $url .= '?' . http_build_query($queryParams);
30        }
31
32        $payload = $body ? json_encode($body, JSON_THROW_ON_ERROR) : '';
33        $timestamp = (string) time();
34
35        // HMAC Signature: hash(method + path + timestamp + payload, api_key)
36        $signaturePayload = $method . $path . $timestamp . $payload;
37        $signature = hash_hmac('sha256', $signaturePayload, $apiRecord->getApiKey());
38
39        $headers = [
40            'Content-Type: application/json',
41            'Accept: application/json',
42            'X-Timestamp: ' . $timestamp,
43            'X-Signature: ' . $signature,
44        ];
45
46        $contextOptions = [
47            'http' => [
48                'method' => $method,
49                'header' => implode("\r\n", $headers),
50                'content' => $payload,
51                'ignore_errors' => true, // allow reading error body
52            ],
53        ];
54
55        $context = stream_context_create($contextOptions);
56
57        $response = file_get_contents($url, false, $context);
58
59        if ($response === false) {
60            throw new \UnexpectedValueException("Failed to connect to API: $url");
61        }
62
63        $decoded = json_decode($response, true);
64        if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) {
65            throw new \UnexpectedValueException("Invalid JSON response from API: $response");
66        }
67
68        return $decoded;
69    }
70}