ChatlasClient::apiRequest()   B
last analyzed

Complexity

Conditions 7
Paths 48

Size

Total Lines 33
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 7
eloc 22
c 3
b 0
f 1
nc 48
nop 1
dl 0
loc 33
rs 8.6346
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * Chatlas BEdita plugin
6
 *
7
 * Copyright 2023 Atlas Srl
8
 */
9
namespace BEdita\Chatlas\Client;
10
11
use Cake\Core\Configure;
12
use Cake\Http\Client;
13
use Cake\Http\Client\FormData;
14
use Cake\Http\Client\Response;
15
use Cake\Http\Exception\HttpException;
16
use Cake\Log\LogTrait;
17
use Throwable;
18
19
/**
20
 * Chatlas API Client.
21
 */
22
class ChatlasClient
23
{
24
    use LogTrait;
25
26
    /**
27
     * API internal HTTP client
28
     *
29
     * @var \Cake\Http\Client
30
     */
31
    protected Client $client;
32
33
    /**
34
     * Default content type in requests
35
     */
36
    public const DEFAULT_CONTENT_TYPE = 'application/json';
37
38
    /**
39
     * Client constructor
40
     */
41
    public function __construct()
42
    {
43
        $this->initialize();
44
    }
45
46
    /**
47
     * Initialize client from `Chatlas` configuration key
48
     *
49
     * @return void
50
     */
51
    public function initialize(): void
52
    {
53
        $options = parse_url((string)Configure::read('Chatlas.apiUrl')) + array_filter([
54
            'headers' => [
55
                'Accept' => 'application/json',
56
            ],
57
            'timeout' => Configure::read('Chatlas.timeout'),
58
            'adapter' => Configure::read('Chatlas.adapter'),
59
        ]);
60
        if (Configure::check('Chatlas.token')) {
61
            $options['headers'] += [
62
                'Authorization' => sprintf('Bearer %s', (string)Configure::read('Chatlas.token')),
63
            ];
64
        }
65
66
        $this->client = new Client($options);
67
    }
68
69
    /**
70
     * Proxy for GET requests to Chatlas API
71
     *
72
     * @param string $path The path for API request
73
     * @param array $query The query params
74
     * @param array<string, string> $headers The request headers
75
     * @return \Cake\Http\Client\Response
76
     */
77
    public function get(string $path = '', array $query = [], array $headers = []): Response
78
    {
79
        return $this->apiRequest(compact('path', 'query', 'headers') + [
80
            'method' => 'get',
81
        ]);
82
    }
83
84
    /**
85
     * Proxy for POST requests to Chatlas API
86
     *
87
     * @param string $path The path for API request
88
     * @param array $body The body data
89
     * @param array<string, string> $headers The request headers
90
     * @return \Cake\Http\Client\Response
91
     */
92
    public function post(string $path = '', array $body = [], array $headers = []): Response
93
    {
94
        return $this->apiRequest(compact('path', 'body', 'headers') + [
95
            'method' => 'post',
96
        ]);
97
    }
98
99
    /**
100
     * Proxy for POST multipart/form requests to Chatlas API
101
     *
102
     * @param string $path The path for API request
103
     * @param \Cake\Http\Client\FormData $form The form data
104
     * @return \Cake\Http\Client\Response
105
     */
106
    public function postMultipart(string $path, FormData $form): Response
107
    {
108
        return $this->apiRequest(compact('path') + [
109
            'method' => 'post',
110
            'body' => (string)$form,
111
            'headers' => ['Content-Type' => $form->contentType()],
112
        ]);
113
    }
114
115
    /**
116
     * Proxy for PATCH requests to Chatlas API
117
     *
118
     * @param string $path The path for API request
119
     * @param array $body The body data
120
     * @param array<string, string> $headers The request headers
121
     * @return \Cake\Http\Client\Response
122
     */
123
    public function patch(string $path = '', array $body = [], array $headers = []): Response
124
    {
125
        return $this->apiRequest(compact('path', 'body', 'headers') + [
126
            'method' => 'patch',
127
        ]);
128
    }
129
130
    /**
131
     * Proxy for DELETE requests to Chatlas API
132
     *
133
     * @param string $path The path for API request
134
     * @param array $body The body data
135
     * @param array<string, string> $headers The request headers
136
     * @return \Cake\Http\Client\Response
137
     */
138
    public function delete(string $path = '', array $body = [], array $headers = []): Response
139
    {
140
        return $this->apiRequest(compact('path', 'body', 'headers') + [
141
            'method' => 'delete',
142
        ]);
143
    }
144
145
    /**
146
     * Routes a request to the API handling response and errors.
147
     *
148
     * `$options` are:
149
     * - method => the HTTP request method
150
     * - path => a string representing the complete endpoint path
151
     * - query => an array of query strings
152
     * - body => the body sent
153
     * - headers => an array of headers
154
     *
155
     * @param array $options The request options
156
     * @return \Cake\Http\Client\Response
157
     */
158
    protected function apiRequest(array $options): Response
159
    {
160
        $options += [
161
            'method' => '',
162
            'path' => '',
163
            'query' => null,
164
            'body' => null,
165
            'headers' => null,
166
        ];
167
168
        if (empty($options['body'])) {
169
            $options['body'] = null;
170
        }
171
        if (is_array($options['body'])) {
172
            $options['body'] = json_encode($options['body']);
173
        }
174
        if (!empty($options['body']) && empty($options['headers']['Content-Type'])) {
175
            $options['headers']['Content-Type'] = static::DEFAULT_CONTENT_TYPE;
176
        }
177
178
        $response = new Response();
179
        $statusCode = 0;
180
        try {
181
            $response = $this->sendRequest($options);
182
            $statusCode = $response->getStatusCode();
183
        } catch (Throwable $e) {
184
            $this->handleError($e->getCode(), $e->getMessage());
185
        }
186
        if ($statusCode >= 400) {
187
            $this->handleError($statusCode, $response->getStringBody());
188
        }
189
190
        return $response;
191
    }
192
193
    /**
194
     * Send request using options
195
     *
196
     * @param array $options Request options array
197
     * @return \Cake\Http\Client\Response
198
     */
199
    protected function sendRequest(array $options): Response
200
    {
201
        $method = strtolower($options['method']);
202
        $headers = ['headers' => (array)$options['headers']];
203
        if ($method === 'get') {
204
            return $this->client->get(
205
                (string)$options['path'],
206
                (array)$options['query'],
207
                $headers
208
            );
209
        }
210
211
        return call_user_func_array(
212
            [$this->client, $method],
213
            [
214
                (string)$options['path'],
215
                $options['body'],
216
                $headers,
217
            ]
218
        );
219
    }
220
221
    /**
222
     * Handle Chatlas API error: log and throw exception
223
     *
224
     * @param int $code Error code
225
     * @param string $message Error message
226
     * @throws \Cake\Http\Exception\HttpException
227
     * @return void
228
     */
229
    protected function handleError(int $code, string $message): void
230
    {
231
        if ($code < 100 || $code > 599) {
232
            $code = 500;
233
        }
234
        $msg = sprintf('Chatlas API error: %s', $message);
235
        $this->log(sprintf('[%d] %s', $code, $msg), 'error');
236
        throw new HttpException($msg, $code);
237
    }
238
}
239