BaseClient   A
last analyzed

Complexity

Total Complexity 21

Size/Duplication

Total Lines 250
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 82
c 1
b 0
f 0
dl 0
loc 250
rs 10
wmc 21

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getHttpClient() 0 3 1
A get() 0 7 1
A put() 0 8 1
A patch() 0 8 1
A getUrl() 0 9 2
B logCall() 0 25 7
A getValidator() 0 7 1
A delete() 0 8 1
A defaultConfigName() 0 6 1
A post() 0 8 1
A createClient() 0 13 1
A __construct() 0 6 1
A validateConf() 0 6 2
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita, API-first content management framework
6
 * Copyright 2025 ChannelWeb Srl, Chialab Srl
7
 *
8
 * This file is part of BEdita: you can redistribute it and/or modify
9
 * it under the terms of the GNU Lesser General Public License as published
10
 * by the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
14
 */
15
16
namespace BEdita\WebTools\Http;
17
18
use Cake\Core\App;
19
use Cake\Core\Configure;
20
use Cake\Core\InstanceConfigTrait;
21
use Cake\Http\Client;
22
use Cake\Http\Client\Response;
23
use Cake\Log\LogTrait;
24
use Cake\Validation\Validator;
25
use InvalidArgumentException;
26
27
/**
28
 * Base class for clients.
29
 */
30
abstract class BaseClient
31
{
32
    use InstanceConfigTrait;
33
    use LogTrait;
34
35
    /**
36
     * Default configuration.
37
     *
38
     * @var array
39
     */
40
    protected array $_defaultConfig = [
41
        'auth' => null,
42
        'logLevel' => 'error',
43
        'url' => null,
44
    ];
45
46
    /**
47
     * The HTTP client.
48
     *
49
     * @var \Cake\Http\Client
50
     */
51
    protected Client $client;
52
53
    /**
54
     * Constructor. Initialize HTTP client.
55
     *
56
     * @param array $config The configuration
57
     */
58
    public function __construct(array $config = [])
59
    {
60
        $config += (array)Configure::read($this->defaultConfigName(), []);
61
        $this->setConfig($config);
62
        $this->validateConf($this->getValidator());
63
        $this->createClient();
64
    }
65
66
    /**
67
     * Get default config name.
68
     * It's the name of the client class without `Client` suffix.
69
     *
70
     * @return string
71
     */
72
    protected function defaultConfigName(): string
73
    {
74
        $shortName = App::shortName(static::class, 'Http', 'Client');
75
        [, $name] = pluginSplit($shortName);
76
77
        return $name;
78
    }
79
80
    /**
81
     * Return the Validator object
82
     *
83
     * @return \Cake\Validation\Validator
84
     */
85
    protected function getValidator(): Validator
86
    {
87
        $validator = new Validator();
88
89
        return $validator
90
            ->requirePresence('url')
91
            ->notEmptyString('url');
92
    }
93
94
    /**
95
     * Validate configuration data.
96
     *
97
     * @param \Cake\Validation\Validator $validator The validator object
98
     * @return void
99
     */
100
    protected function validateConf(Validator $validator): void
101
    {
102
        $errors = $validator->validate($this->getConfig());
103
        if (!empty($errors)) {
104
            throw new InvalidArgumentException(
105
                sprintf('%s client config not valid: %s', static::class, json_encode($errors))
106
            );
107
        }
108
    }
109
110
    /**
111
     * Create JSON HTTP client.
112
     *
113
     * @return void
114
     */
115
    protected function createClient(): void
116
    {
117
        $parsedUrl = parse_url($this->getConfig('url'));
118
        $options = [
119
            'host' => $parsedUrl['host'],
120
            'scheme' => $parsedUrl['scheme'],
121
            'path' => $parsedUrl['path'] ?? '',
122
            'headers' => [
123
                'Accept' => 'application/json',
124
                'Content-Type' => 'application/json',
125
            ],
126
        ] + $this->getConfig();
127
        $this->client = new Client($options);
128
    }
129
130
    /**
131
     * Get the correct relative url.
132
     *
133
     * @param string $url The relative url
134
     * @return string
135
     */
136
    protected function getUrl(string $url): string
137
    {
138
        if (strpos($url, 'https://') === 0) {
139
            return $url;
140
        }
141
        $base = trim($this->client->getConfig('path'), '/');
142
        $url = trim($url, '/');
143
144
        return sprintf('%s/%s', $base, $url);
145
    }
146
147
    /**
148
     * Log API call.
149
     *
150
     * @param string $call The API call
151
     * @param string $url The API url
152
     * @param string $payload The json payload
153
     * @param \Cake\Http\Client\Response $response The API response
154
     * @return ?string
155
     */
156
    protected function logCall(string $call, string $url, string $payload, Response $response): ?string
157
    {
158
        $level = $this->getConfig('logLevel') ?? 'error';
159
        if (!in_array($level, ['error', 'debug'])) {
160
            return null;
161
        }
162
        $result = $response->isOk() ? '' : 'error';
163
        if ($level === 'error' && empty($result)) {
164
            return null;
165
        }
166
        $level = $result === 'error' ? 'error' : $level;
167
        $message = sprintf(
168
            '%s API %s | %s %s | with status %s: %s - Payload: %s',
169
            $response->isOk() ? '[OK]' : '[ERROR]',
170
            $this->defaultConfigName(),
171
            $call,
172
            $url,
173
            $response->getStatusCode(),
174
            (string)$response->getBody(),
175
            $payload
176
        );
177
        $message = trim($message);
178
        $this->log($message, $level);
179
180
        return $message;
181
    }
182
183
    /**
184
     * Get http client
185
     *
186
     * @return \Cake\Http\Client The client
187
     */
188
    public function getHttpClient(): Client
189
    {
190
        return $this->client;
191
    }
192
193
    /**
194
     * Get request.
195
     *
196
     * @param string $url The request url
197
     * @param array $data The query data
198
     * @param array $options Request options
199
     * @return \Cake\Http\Client\Response
200
     */
201
    public function get(string $url, array $data = [], array $options = []): Response
202
    {
203
        $apiUrl = $this->getUrl($url);
204
        $response = $this->client->get($apiUrl, $data, $options);
205
        $this->logCall('/GET', $apiUrl, json_encode($data), $response);
206
207
        return $response;
208
    }
209
210
    /**
211
     * Post request.
212
     *
213
     * @param string $url The request url
214
     * @param array $data The post data
215
     * @param array $options Request options
216
     * @return \Cake\Http\Client\Response
217
     */
218
    public function post(string $url, array $data = [], array $options = []): Response
219
    {
220
        $data = json_encode($data);
221
        $apiUrl = $this->getUrl($url);
222
        $response = $this->client->post($apiUrl, $data, $options);
223
        $this->logCall('/POST', $apiUrl, $data, $response);
224
225
        return $response;
226
    }
227
228
    /**
229
     * Patch request.
230
     *
231
     * @param string $url The request url
232
     * @param array $data The post data
233
     * @param array $options Request options
234
     * @return \Cake\Http\Client\Response
235
     */
236
    public function patch(string $url, array $data = [], array $options = []): Response
237
    {
238
        $apiUrl = $this->getUrl($url);
239
        $data = json_encode($data);
240
        $response = $this->client->patch($apiUrl, $data, $options);
241
        $this->logCall('/PATCH', $apiUrl, $data, $response);
242
243
        return $response;
244
    }
245
246
    /**
247
     * Put request.
248
     *
249
     * @param string $url The request url
250
     * @param array $data The post data
251
     * @param array $options Request options
252
     * @return \Cake\Http\Client\Response
253
     */
254
    public function put(string $url, array $data = [], array $options = []): Response
255
    {
256
        $apiUrl = $this->getUrl($url);
257
        $data = json_encode($data);
258
        $response = $this->client->put($this->getUrl($url), $data, $options);
259
        $this->logCall('/PUT', $apiUrl, $data, $response);
260
261
        return $response;
262
    }
263
264
    /**
265
     * Delete request.
266
     *
267
     * @param string $url The request url
268
     * @param array $data The post data
269
     * @param array $options Request options
270
     * @return \Cake\Http\Client\Response
271
     */
272
    public function delete(string $url, array $data = [], array $options = []): Response
273
    {
274
        $apiUrl = $this->getUrl($url);
275
        $data = json_encode($data);
276
        $response = $this->client->delete($this->getUrl($url), $data, $options);
277
        $this->logCall('/DELETE', $apiUrl, $data, $response);
278
279
        return $response;
280
    }
281
}
282