Passed
Push — main ( 55f6d1...f06863 )
by Daryl
02:57
created

Api   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 416
Duplicated Lines 0 %

Test Coverage

Coverage 89.12%

Importance

Changes 11
Bugs 0 Features 0
Metric Value
wmc 42
eloc 120
c 11
b 0
f 0
dl 0
loc 416
ccs 131
cts 147
cp 0.8912
rs 9.0399

29 Methods

Rating   Name   Duplication   Size   Complexity  
A setLogger() 0 3 1
A setCache() 0 3 1
A setClient() 0 3 1
A setMachine() 0 3 1
A getClient() 0 3 1
A logger() 0 3 1
A baseRoute() 0 3 1
A getRequestArgs() 0 28 3
A __construct() 0 33 5
A setVersion() 0 3 1
A getMachine() 0 3 1
A getPassword() 0 3 1
A setPassword() 0 3 1
A getLogger() 0 3 1
A getBaseRoute() 0 3 1
A getUri() 0 4 1
A setUsergroup() 0 3 1
A getCache() 0 3 1
A setBaseRoute() 0 3 1
A getUsergroup() 0 3 1
A getAuthorizationHeaderValue() 0 12 1
A getVersion() 0 3 1
A post() 0 8 1
A logEvent() 0 11 2
A setUsername() 0 3 1
A get() 0 7 1
A generateCacheKey() 0 15 1
A getUsername() 0 3 1
B makeRequest() 0 51 7

How to fix   Complexity   

Complex Class

Complex classes like Api often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Api, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Clubdeuce\Tessitura\Helpers;
4
5
use Clubdeuce\Tessitura\Base\Base;
6
use Clubdeuce\Tessitura\Interfaces\ApiInterface;
7
use Clubdeuce\Tessitura\Interfaces\CacheInterface;
8
use Clubdeuce\Tessitura\Interfaces\LoggerAwareInterface;
9
use Exception;
10
use GuzzleHttp\Client;
11
use GuzzleHttp\Exception\GuzzleException;
12
use GuzzleHttp\Psr7\Stream;
13
use Psr\Log\LoggerInterface;
14
15
/**
16
 * Class API
17
 * @package Clubdeuce\Tessitura\Helpers
18
 */
19
class Api extends Base implements
20
    ApiInterface,
21
    LoggerAwareInterface
22
{
23
    protected const CACHE_EXPIRATION_DEFAULT = 10 * 60; // 10 minutes
24
25
    // These are the parameters that are required to connect to the Tessitura API.
26
    protected string $_base_route = '';
27
28
    protected string $_machine;
29
30
    protected string $_password;
31
32
    protected string $_username;
33
34
    /**
35
     * @var string The usergroup required for authentication
36
     */
37
    protected string $_usergroup;
38
39
    /**
40
     * @var string The Tessitura API version to use with this library
41
     */
42
    protected string $_version = '15';
43
44
    /**
45
     * @var Client GuzzleHttp client
46
     */
47
    protected Client $_client;
48
49
    /**
50
     * @var LoggerInterface|null
51
     */
52
    protected ?LoggerInterface $_logger = null;
53
54
    /**
55
     * @var CacheInterface|null
56
     */
57
    protected ?CacheInterface $_cache = null;
58
59
    /**
60
     * API constructor.
61
     *
62
     * @param mixed[] $args {
63
     * @type string   $base_route
64
     * @type string   $location
65
     * @type string   $password
66
     * @type string   $usergroup
67
     * @type string   $username
68
     * @type string   $version
69
     * }
70
     * @param Client|null $client The HTTP client to use for API requests
71
     * @param LoggerInterface|null $logger The logger to use for logging
72
     * @param CacheInterface|null $cache The cache implementation to use for caching API responses
73
     */
74 21
    public function __construct(
75
        array $args = [],
76
        ?Client $client = null,
77
        ?LoggerInterface $logger = null,
78
        ?CacheInterface $cache = null
79
    ) {
80 21
        $args = $this->parseArgs($args, [
81 21
            'baseRoute' => '',
82 21
            'machine'   => '',
83 21
            'password'  => '',
84 21
            'usergroup' => '',
85 21
            'username'  => '',
86 21
            'version'   => '16',
87 21
        ]);
88
89 21
        if ($logger) {
90
            $this->setLogger($logger);
91
        }
92
93 21
        if ($cache) {
94 5
            $this->setCache($cache);
95
        }
96
97 21
        if (!$client && !empty($args['baseRoute'])) {
98 3
            $client = new Client([
99 3
                'base_uri' => $args['baseRoute'],
100 3
                'timeout'  => 10.0,
101 3
            ]);
102
        }
103
104 21
        $args['client'] = $client;
105
106 21
        parent::__construct($args);
107
    }
108
109
    /**
110
     * @param string $resource
111
     * @param mixed[] $args
112
     * @return mixed
113
     * @throws Exception
114
     */
115 6
    public function get(string $resource, array $args = []): mixed
116
    {
117 6
        $args = array_merge($args, [
118 6
            'method' => 'GET',
119 6
        ]);
120
121 6
        return $this->makeRequest($resource, $args);
122
    }
123
124
    /**
125
     * @param string  $endpoint
126
     * @param mixed[] $args
127
     * @return mixed[]
128
     * @throws Exception|GuzzleException
129
     */
130 7
    protected function makeRequest(string $endpoint, array $args): array
131
    {
132 7
        $args = $this->parseArgs($args, [
133 7
            'method' => 'GET',
134 7
        ]);
135
136 7
        $method   = $args['method'];
137 7
        $cacheKey = $this->generateCacheKey($endpoint, $args);
138
139
        // Only cache GET requests
140 7
        if ($method === 'GET' && $this->_cache) {
141 4
            $cachedResponse = $this->_cache->get($cacheKey);
142
143 4
            if ($cachedResponse !== null) {
144 2
                $this->logEvent('Cache hit for endpoint: ' . $endpoint);
145
146 2
                return $cachedResponse;
147
            }
148
        }
149
150
        // Use the appropriate HTTP method
151 6
        $response = match ($method) {
152 1
            'POST'  => $this->_client->post($this->getUri($endpoint), $args),
153 5
            'GET'   => $this->_client->get($this->getUri($endpoint), $args),
154
            default => throw new Exception("Unsupported HTTP method: {$method}"),
155 6
        };
156
157 5
        if (200 === $response->getStatusCode()) {
158 5
            $data = json_decode($response->getBody(), true);
159
160
            // Cache successful GET responses
161 5
            if ($method === 'GET' && $this->_cache) {
162 3
                $cacheExpiration = $args['cache_expiration'] ?? self::CACHE_EXPIRATION_DEFAULT;
163 3
                $this->_cache->set($cacheKey, $data, $cacheExpiration);
164 3
                $this->logEvent('Cached response for endpoint: ' . $endpoint);
165
            }
166
167 5
            return $data;
168
        }
169
170
        // We have successfully gotten a response from the API, but not a 200 status code.
171
        /**
172
         * @var Stream $body
173
         */
174
        $body = $response->getBody();
175
176
        $this->logEvent("Error response from endpoint: {$endpoint}. {$body->getContents()}");
177
178
        throw new Exception(
179
            $body->getContents(),
180
            $response->getStatusCode()
181
        );
182
    }
183
184 9
    public function getVersion(): string
185
    {
186 9
        return $this->_version;
187
    }
188
189
    /**
190
     * @param mixed[] $args
191
     *
192
     * @return mixed[] {
193
     * @type int $cache_expiration
194
     * @type int $timeout
195
     * @type array $headers
196
     * }
197
     */
198 1
    protected function getRequestArgs(array $args = []): array
199
    {
200
201 1
        $args = $this->parseArgs($args, [
202 1
            'cache_expiration' => self::CACHE_EXPIRATION_DEFAULT,
203 1
            'headers'          => [],
204 1
            'body'             => '',
205 1
            'timeout'          => 10.0,
206 1
        ]);
207
208 1
        if (is_array($args['body'])) {
209
            if (empty($args['body'])) {
210
                $args['body'] = null;
211
            } else {
212
                $args['body'] = json_encode($args['body']);
213
            }
214
        }
215
216 1
        $parsedUrl       = parse_url($this->baseRoute());
217 1
        $args['headers'] = $this->parseArgs($args['headers'], [
218 1
            'Authorization'  => $this->getAuthorizationHeaderValue(),
219 1
            'Content-Type'   => 'application/json',
220 1
            'Content-Length' => strlen($args['body']),
221 1
            'Accept'         => 'application/json',
222 1
            'Host'           => $parsedUrl['host'] ?? $this->baseRoute(),
223 1
        ]);
224
225 1
        return array_filter($args);
226
    }
227
228
    /**
229
     * @return string
230
     */
231 2
    protected function getAuthorizationHeaderValue(): string
232
    {
233
234 2
        $auth_key = sprintf(
235 2
            '%1$s:%2$s:%3$s:%4$s',
236 2
            $this->getUsername(),
237 2
            $this->getUsergroup(),
238 2
            $this->getMachine(),
239 2
            $this->getPassword()
240 2
        );
241
242 2
        return sprintf('Basic %1$s', base64_encode($auth_key));
243
    }
244
245
    /**
246
     * @param string $endpoint
247
     *
248
     * @return string
249
     */
250 7
    protected function getUri(string $endpoint): string
251
    {
252
253 7
        return "{$this->baseRoute()}/{$endpoint}";
254
    }
255
256
    /**
257
     * @param string $endpoint
258
     * @param mixed[] $args
259
     * @return Exception|mixed[]
260
     * @throws GuzzleException
261
     */
262 1
    public function post(string $endpoint, array $args = []): array|Exception
263
    {
264
265 1
        $args = array_merge($args, [
266 1
            'method' => 'POST',
267 1
        ]);
268
269 1
        return $this->makeRequest($endpoint, $args);
270
    }
271
272
    /**
273
     * @param string $message
274
     * @param mixed[] $args {
275
     * @type string $file
276
     * @type string $line
277
     * @type string $function
278
     * @type array $trace
279
     * @type mixed[] $extra
280
     * }
281
     */
282 4
    protected function logEvent(string $message, array $args = []): void
283
    {
284
285 4
        $args = $this->parseArgs($args, [
286 4
            'log' => 'tessitura',
287 4
        ]);
288
289 4
        $message = 'Tessitura API: ' . $message;
290
291 4
        if ($this->getLogger()) {
292
            $this->getLogger()->info($message, $args);
293
        }
294
    }
295
296
    /**
297
     * Sets a logger instance on the object.
298
     *
299
     * @param LoggerInterface $logger The logger instance to use.
300
     * @return void
301
     */
302 1
    public function setLogger(LoggerInterface $logger): void
303
    {
304 1
        $this->_logger = $logger;
305
    }
306
307
    /**
308
     * Gets the logger instance.
309
     *
310
     * @return LoggerInterface|null The logger instance or null if none is set.
311
     */
312 5
    public function getLogger(): ?LoggerInterface
313
    {
314 5
        return $this->_logger;
315
    }
316
317
    public function logger(): ?LoggerInterface
318
    {
319
        return $this->getLogger();
320
    }
321
322
    public function setClient(Client $client): void
323
    {
324
        $this->_client = $client;
325
    }
326
327 1
    public function getClient(): Client
328
    {
329 1
        return $this->_client;
330
    }
331
332 10
    public function baseRoute(): string
333
    {
334 10
        return $this->_base_route;
335
    }
336
337 1
    public function setBaseRoute(string $baseRoute): void
338
    {
339 1
        $this->_base_route = $baseRoute;
340
    }
341
342 1
    public function setMachine(string $machine): void
343
    {
344 1
        $this->_machine = $machine;
345
    }
346
347 1
    public function setPassword(string $password): void
348
    {
349 1
        $this->_password = $password;
350
    }
351
352 1
    public function setUsername(string $username): void
353
    {
354 1
        $this->_username = $username;
355
    }
356
357 1
    public function setUsergroup(string $usergroup): void
358
    {
359 1
        $this->_usergroup = $usergroup;
360
    }
361
362 1
    public function setVersion(string $version): void
363
    {
364 1
        $this->_version = $version;
365
    }
366
367 1
    public function getBaseRoute(): string
368
    {
369 1
        return $this->_base_route;
370
    }
371
372 3
    public function getMachine(): string
373
    {
374 3
        return $this->_machine;
375
    }
376
377 3
    public function getPassword(): string
378
    {
379 3
        return $this->_password;
380
    }
381
382 4
    public function getUsergroup(): string
383
    {
384 4
        return $this->_usergroup;
385
    }
386
387 3
    public function getUsername(): string
388
    {
389 3
        return $this->_username;
390
    }
391
392
    /**
393
     * Sets a cache instance on the object.
394
     *
395
     * @param CacheInterface $cache The cache instance to use.
396
     * @return void
397
     */
398 6
    public function setCache(CacheInterface $cache): void
399
    {
400 6
        $this->_cache = $cache;
401
    }
402
403
    /**
404
     * Gets the cache instance.
405
     *
406
     * @return CacheInterface|null The cache instance or null if none is set.
407
     */
408 1
    public function getCache(): ?CacheInterface
409
    {
410 1
        return $this->_cache;
411
    }
412
413
    /**
414
     * Generate a cache key for the given endpoint and arguments.
415
     *
416
     * @param string   $endpoint The API endpoint
417
     * @param string[] $args The request arguments
418
     * @return string  The generated cache key
419
     */
420 8
    protected function generateCacheKey(string $endpoint, array $args): string
421
    {
422
        // Remove method and cache-specific args from key generation
423 8
        $keyArgs = $args;
424 8
        unset($keyArgs['method'], $keyArgs['cache_expiration']);
425
426
        // Create a consistent key based on endpoint and args
427 8
        $keyData = [
428 8
            'endpoint'   => $endpoint,
429 8
            'base_route' => $this->baseRoute(),
430 8
            'version'    => $this->getVersion(),
431 8
            'args'       => $keyArgs,
432 8
        ];
433
434 8
        return 'tessitura:' . md5(json_encode($keyData));
435
    }
436
}
437