Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.28% covered (success)
98.28%
57 / 58
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiClient
98.28% covered (success)
98.28%
57 / 58
75.00% covered (warning)
75.00%
3 / 4
16
0.00% covered (danger)
0.00%
0 / 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%
24 / 24
100.00% covered (success)
100.00%
1 / 1
8
 sendCurlRequest
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
5.01
 sendStreamRequest
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace App\Shared\Service;
6
7use App\Domain\Api\Repository\ApiRecordRepository;
8use App\Domain\Host\Repository\HostRecordRepository;
9
10 class ApiClient
11{
12    public function __construct(
13        private HostRecordRepository $hostRecordRepository,
14        private ApiRecordRepository $apiRecordRepository
15    ) {}
16
17    /**
18     * @param array<string, mixed> $data
19     * @return array<string, mixed>
20     */
21    public function request(string $hostId, string $method, string $endpoint, array $data = []): array
22    {
23        $host = $this->hostRecordRepository->findById($hostId);
24        $apiRecord = $this->apiRecordRepository->findByHostId($hostId);
25
26        $baseUrl = $apiRecord?->getEndpointUrl() ?? $host?->getUrl() ?? 'http://127.0.0.1:8080';
27        $apiKey = $apiRecord?->getApiKey() ?? 'ammonly_server_secret_key';
28
29        $url = rtrim($baseUrl, '/') . '/' . ltrim($endpoint, '/');
30        $timestamp = (string) time();
31        $body = !empty($data) ? (string) json_encode($data, JSON_THROW_ON_ERROR) : '';
32
33        $pathParsed = parse_url($url, PHP_URL_PATH);
34        $path = is_string($pathParsed) && $pathParsed !== '' ? $pathParsed : $endpoint;
35
36        $payload = $timestamp . strtoupper($method) . $path . $body;
37        $signature = hash_hmac('sha256', $payload, $apiKey);
38
39        $headers = [
40            'Content-Type' => 'application/json',
41            'X-Api-Key' => $hostId,
42            'X-Api-Timestamp' => $timestamp,
43            'X-Api-Signature' => $signature,
44        ];
45
46        $result = function_exists('curl_init')
47            ? $this->sendCurlRequest($url, $method, $headers, $body)
48            : $this->sendStreamRequest($url, $method, $headers, $body);
49
50        if ($result === false || $result === '') {
51            return [];
52        }
53
54        $decoded = json_decode((string) $result, true);
55        return is_array($decoded) ? $decoded : [];
56    }
57
58    /**
59     * @param array<string, string> $headers
60     */
61    private function sendCurlRequest(string $url, string $method, array $headers, string $body): string|false
62    {
63        $curlHeaders = [];
64        foreach ($headers as $key => $val) {
65            $curlHeaders[] = $key . ': ' . $val;
66        }
67
68        $ch = curl_init($url);
69        if ($ch === false) {
70            return false;
71        }
72        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method));
73        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
74        curl_setopt($ch, CURLOPT_HTTPHEADER, $curlHeaders);
75        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
76        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
77
78        if ($body !== '') {
79            curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
80        }
81
82        $result = curl_exec($ch);
83        curl_close($ch);
84
85        return is_string($result) ? $result : false;
86    }
87
88    /**
89     * @param array<string, string> $headers
90     */
91    private function sendStreamRequest(string $url, string $method, array $headers, string $body): string|false
92    {
93        $headerLines = [];
94        foreach ($headers as $key => $val) {
95            $headerLines[] = $key . ': ' . $val;
96        }
97
98        $opts = [
99            'http' => [
100                'method' => strtoupper($method),
101                'header' => implode("\r\n", $headerLines),
102                'content' => $body,
103                'ignore_errors' => true,
104            ],
105            'ssl' => [
106                'verify_peer' => true,
107                'verify_peer_name' => true,
108            ],
109        ];
110
111        $context = stream_context_create($opts);
112        return @file_get_contents($url, false, $context);
113    }
114}