AbstractClient::processClientOptions()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 8
rs 10
ccs 6
cts 6
cp 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of Esi\LibrariesIO.
7
 *
8
 * (c) 2023-2024 Eric Sizemore <https://github.com/ericsizemore>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13
14
namespace Esi\LibrariesIO;
15
16
use Esi\LibrariesIO\Exception\InvalidApiKeyException;
17
use Esi\LibrariesIO\Exception\RateLimitExceededException;
18
use GuzzleHttp\Client;
19
use GuzzleHttp\Exception\ClientException;
20
use GuzzleHttp\Exception\GuzzleException;
21
use GuzzleHttp\Exception\InvalidArgumentException;
22
use GuzzleHttp\Handler\MockHandler;
23
use GuzzleHttp\HandlerStack;
24
use Kevinrob\GuzzleCache\CacheMiddleware;
25
use Kevinrob\GuzzleCache\Storage\Psr6CacheStorage;
26
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
27
use Psr\Http\Message\ResponseInterface;
28
use SensitiveParameter;
29
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
30
31
use function array_filter;
32
use function array_merge;
33
use function preg_match;
34
35
use const ARRAY_FILTER_USE_BOTH;
36
37
abstract class AbstractClient
38
{
39
    public const LIB_VERSION = '2.0.0';
40
41
    private const API_URL = 'https://libraries.io/api/';
42
43
    /**
44
     * @see https://libraries.io/account
45
     */
46
    private readonly string $apiKey;
47
48
    private readonly Client $client;
49
50
    /**
51
     * @param string               $apiKey        Your Libraries.io API Key
52
     * @param ?string              $cachePath     The path to your cache on the filesystem
53
     * @param array<string, mixed> $clientOptions An associative array with options to set in the initial config
54
     *                                            of the Guzzle client.
55
     *
56
     * @see https://docs.guzzlephp.org/en/stable/request-options.html
57
     *
58
     * @throws InvalidApiKeyException
59
     * @throws InvalidArgumentException
60
     */
61 40
    protected function __construct(#[SensitiveParameter] string $apiKey, ?string $cachePath = null, ?array $clientOptions = null)
62
    {
63 40
        if (preg_match('/^[0-9a-fA-F]{32}$/', $apiKey) === 0) {
64 1
            throw new InvalidApiKeyException('API key typically consists of alpha numeric characters and is 32 chars in length');
65
        }
66
67 39
        $this->apiKey = $apiKey;
68
69 39
        $clientOptions = self::processClientOptions($clientOptions);
70
71
        // To ease unit testing and handle cache...
72
        /** @var array{_mockHandler?: MockHandler} $clientOptions */
73 39
        $handlerStack = HandlerStack::create($clientOptions['_mockHandler'] ?? null);
74 39
        unset($clientOptions['_mockHandler']);
75
76 39
        $cachePath = Utils::validateCachePath($cachePath);
77
78 39
        if ($cachePath !== null) {
79 39
            $handlerStack->push(new CacheMiddleware(new PrivateCacheStrategy(
80 39
                new Psr6CacheStorage(new FilesystemAdapter('libIo', 300, $cachePath))
81 39
            )), 'cache');
82
        }
83
84 39
        $this->client = new Client(array_merge([
85 39
            'base_uri'    => self::API_URL,
86 39
            'headers'     => ['Accept' => 'application/json', ],
87 39
            'handler'     => $handlerStack,
88 39
            'http_errors' => true,
89 39
            'timeout'     => 10,
90 39
            'query'       => ['api_key' => $this->apiKey, ],
91 39
        ], $clientOptions));
92
    }
93
94
    /**
95
     * @param null|array<array-key, mixed> $options An associative array with options to set in the request.
96
     *
97
     * @see https://docs.guzzlephp.org/en/stable/request-options.html
98
     *
99
     * @throws GuzzleException
100
     * @throws ClientException
101
     * @throws RateLimitExceededException
102
     */
103 31
    protected function request(string $method, string $endpoint, ?array $options = null): ResponseInterface
104
    {
105 31
        $endpoint = Utils::normalizeEndpoint($endpoint, self::API_URL);
106 31
        $method   = Utils::normalizeMethod($method);
107
108 31
        $requestOptions = [
109 31
            'query' => [
110 31
                'api_key' => $this->apiKey,
111 31
            ],
112 31
        ];
113
114 31
        if (isset($options['query']) && \is_array($options['query'])) {
115 25
            $requestOptions['query'] += $options['query'];
116
117 25
            unset($options['query']);
118
        }
119
120 31
        $options        = self::processClientOptions($options);
121 31
        $requestOptions = array_merge($requestOptions, $options);
122
123
        try {
124 31
            return $this->client->request($method, $endpoint, $requestOptions);
125 3
        } catch (ClientException $clientException) {
126 3
            if ($clientException->getResponse()->getStatusCode() === 429) {
127 2
                throw new RateLimitExceededException($clientException);
128
            }
129
130 1
            throw $clientException;
131
        }
132
    }
133
134
    /**
135
     * @param null|array<array-key, mixed> $clientOptions
136
     *
137
     * @return array<array-key, mixed>|array{}
138
     */
139 40
    private static function processClientOptions(?array $clientOptions): array
140
    {
141 40
        $clientOptions ??= [];
142
143 40
        return array_filter($clientOptions, static fn ($value, $key) => match ($key) {
144 1
            'base_uri', 'handler', 'http_errors', 'query' => false, // do not override these default options
145 40
            default => true
146 40
        }, ARRAY_FILTER_USE_BOTH);
147
    }
148
}
149