Passed
Push — master ( 5620bf...f38109 )
by Eric
01:56
created

AbstractClient::__construct()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 30
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 4

Importance

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