Passed
Push — master ( c9356f...ede02a )
by Eric
02:04
created

Api::validateAndDecodeResponse()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 24
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 8
c 0
b 0
f 0
nc 5
nop 2
dl 0
loc 24
ccs 9
cts 9
cp 1
crap 5
rs 9.6111
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of the Numverify API Client for PHP.
7
 *
8
 * (c) 2024 Eric Sizemore <[email protected]>
9
 * (c) 2018-2021 Mark Rogoyski <[email protected]>
10
 *
11
 * @license The MIT License
12
 *
13
 * For the full copyright and license information, please view the LICENSE.md
14
 * file that was distributed with this source code.
15
 */
16
17
namespace Numverify;
18
19
use GuzzleHttp\{
20
    Client,
21
    ClientInterface,
22
    Exception\GuzzleException,
23
    Exception\ServerException,
24
    HandlerStack
25
};
26
use Kevinrob\GuzzleCache\{
27
    CacheMiddleware,
28
    Storage\Psr6CacheStorage,
29
    Strategy\PrivateCacheStrategy
30
};
31
use Numverify\{
32
    Country\Collection,
33
    Country\Country,
34
    Exception\NumverifyApiFailureException,
35
    PhoneNumber\Factory,
36
    PhoneNumber\PhoneNumberInterface
37
};
38
use Psr\Http\Message\ResponseInterface;
39
use SensitiveParameter;
40
use stdClass;
41
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
42
43
use function array_keys;
44
use function array_map;
45
use function array_merge;
46
use function is_dir;
47
use function is_writable;
48
use function json_decode;
49
use function trim;
50
51
/**
52
 * Main API class.
53
 *
54
 * @see \Numverify\Tests\ApiTest
55
 *
56
 * @psalm-api
57
 *
58
 * @phpstan-type ApiJsonArray array{success?: bool, error?: array{code?: int, type?: string, info?: string}, valid?: bool, number?: string, local_format?: string, international_format?: string, country_prefix?: string, country_code?: string, country_name?: string, location?: string, carrier?: string, line_type?: string}
59
 * @phpstan-type ApiCountryJsonArray array<string, array{country_name: string, dialling_code: string}>
60
 */
61
class Api
62
{
63
    /**
64
     * Current version of the Numverify package.
65
     *
66
     * @var string
67
     */
68
    public const LIBRARY_VERSION = '3.0.1';
69
70
    /**
71
     * URL for Numverify's "Free" plan.
72
     *
73
     * @see https://numverify.com/product
74
     */
75
    private const HTTP_URL = 'http://apilayer.net/api';
76
77
    /**
78
     * URL for Numverify's paid plans. ("Basic", "Professional", or "Enterprise").
79
     *
80
     * @see https://numverify.com/product
81
     */
82
    private const HTTPS_URL = 'https://apilayer.net/api';
83
84
    /**
85
     * Guzzle Client.
86
     */
87
    private ClientInterface $client;
88
89
    /**
90
     * Api constructor.
91
     *
92
     * Requires an access key. You can get one from Numverify:
93
     *
94
     * @see https://numverify.com/product
95
     *
96
     * Note: If you are on their free plan, $useHttps = true will not work for you.
97
     *
98
     * @param string               $accessKey API access key.
99
     * @param bool                 $useHttps  (optional) Flag to determine if API calls should use http or https.
100
     * @param ClientInterface|null $client    (optional) Parameter to provide your own Guzzle client.
101
     * @param array<string, mixed> $options   (optional) Array of options to pass to the Guzzle client.
102
     */
103 34
    public function __construct(
104
        #[SensitiveParameter]
105
        private readonly string $accessKey,
106
        bool $useHttps = false,
107
        ?ClientInterface $client = null,
108
        array $options = []
109
    ) {
110
        // If we already have a client.
111 34
        if ($client instanceof ClientInterface) {
112 2
            $this->client = $client;
113
114 2
            return;
115
        }
116
117
        // Build client
118 32
        $clientOptions = ['base_uri' => self::getUrl($useHttps)];
119
120
        // If $options has 'cachePath' key, and it is a valid directory, then buildCacheHandler() will
121
        // add Cache to the Guzzle handler stack.
122 32
        $options = self::buildCacheHandler($options);
123
124
        // Merge $options into main client options.
125 32
        $clientOptions = array_merge($clientOptions, $options);
126
127 32
        $this->client = new Client($clientOptions);
128
    }
129
130
    /**
131
     * Validate a phone number.
132
     *
133
     * Will return ValidPhoneNumber for a valid number, InvalidPhoneNumber otherwise (both PhoneNumberInterface).
134
     *
135
     * @param string $countryCode (Optional) Use to provide a phone number in a local format (non E.164).
136
     *
137
     * @throws NumverifyApiFailureException If the response is non 200 or success field is false.
138
     * @throws GuzzleException              If Guzzle encounters an issue.
139
     */
140 14
    public function validatePhoneNumber(string $phoneNumber, string $countryCode = ''): PhoneNumberInterface
141
    {
142 14
        $phoneNumber = trim($phoneNumber);
143 14
        $countryCode = trim($countryCode);
144
145 14
        $query = [
146 14
            'access_key' => $this->accessKey,
147 14
            'number'     => $phoneNumber,
148 14
        ];
149
150 14
        if ($countryCode !== '') {
151 2
            $query['country_code'] = $countryCode;
152
        }
153
154
        try {
155 14
            $result = $this->client->request('GET', '/validate', [
156 14
                'query' => $query,
157 14
            ]);
158 2
        } catch (ServerException $serverException) {
159
            // >= 400 <= 500 status code
160
            // wrap ServerException with NumverifyApiFailureException
161 2
            throw new NumverifyApiFailureException($serverException->getResponse());
162
        }
163
164
        /** @var stdClass $body */
165 12
        $body = self::validateAndDecodeResponse($result);
166
167 8
        return Factory::create($body);
168
    }
169
170
    /**
171
     * Get list of countries.
172
     *
173
     * @throws NumverifyApiFailureException If the response is non 200 or success field is false.
174
     * @throws GuzzleException              If Guzzle encounters an issue.
175
     */
176 12
    public function getCountries(): Collection
177
    {
178
        try {
179 12
            $response = $this->client->request('GET', '/countries', [
180 12
                'query' => [
181 12
                    'access_key' => $this->accessKey,
182 12
                ],
183 12
            ]);
184 2
        } catch (ServerException $serverException) {
185
            // >= 400 <= 500 status code
186
            // wrap ServerException with NumverifyApiFailureException
187 2
            throw new NumverifyApiFailureException($serverException->getResponse());
188
        }
189
190
        /** @var ApiCountryJsonArray $body */
191 10
        $body = self::validateAndDecodeResponse($response, true);
192
193 6
        $countries = array_map(
194 6
            static fn (array $country, string $countryCode): Country => new Country($countryCode, $country['country_name'], $country['dialling_code']),
195 6
            $body,
1 ignored issue
show
Bug introduced by
$body of type Numverify\ApiCountryJsonArray is incompatible with the type array expected by parameter $array of array_map(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

195
            /** @scrutinizer ignore-type */ $body,
Loading history...
196 6
            array_keys($body)
1 ignored issue
show
Bug introduced by
$body of type Numverify\ApiCountryJsonArray is incompatible with the type array expected by parameter $array of array_keys(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

196
            array_keys(/** @scrutinizer ignore-type */ $body)
Loading history...
197 6
        );
198
199 6
        return new Collection(...$countries);
200
    }
201
202
    /**
203
     * Get the URL to use for API calls.
204
     */
205 32
    private static function getUrl(bool $useHttps): string
206
    {
207 32
        return $useHttps ? self::HTTPS_URL : self::HTTP_URL;
208
    }
209
210
    /**
211
     * Given a response object, checks the status code and checks the 'success' field, if it exists.
212
     *
213
     * If everything looks good, it returns the decoded jSON data based on $asArray.
214
     *
215
     * @param bool $asArray If true, returns the decoded jSON as an assoc. array, stdClass otherwise.
216
     *
217
     * @return stdClass | ApiJsonArray | ApiCountryJsonArray
218
     *
219
     * @throws NumverifyApiFailureException If the response is non 200 or success field is false.
220
     */
221 22
    private static function validateAndDecodeResponse(ResponseInterface $response, bool $asArray = false): stdClass | array
222
    {
223
        // If not 200 ok
224 22
        if ($response->getStatusCode() !== 200) {
225 4
            throw new NumverifyApiFailureException($response);
226
        }
227
228
        /**
229
         * @var ApiJsonArray | ApiCountryJsonArray | stdClass $body
230
         */
231 18
        $body = json_decode($response->getBody()->getContents(), $asArray);
232
233
        /**
234
         * @var ApiJsonArray | ApiCountryJsonArray $data
235
         */
236 18
        $data = $asArray ? $body : (array) $body;
237
238 18
        if (isset($data['success']) && $data['success'] === false) {
239 4
            throw new NumverifyApiFailureException($response);
240
        }
241
242 14
        unset($data);
243
244 14
        return $body;
245
    }
246
247
    /**
248
     * Creates a Guzzle HandlerStack and adds the CacheMiddleware if 'cachePath' exists within the
249
     * given $options array, and it is a valid directory.
250
     *
251
     * Returns given $options as passed, minus the 'cachePath' as it is not a valid Guzzle option for
252
     * the client.
253
     *
254
     * @param array<string, mixed> $options
255
     *
256
     * @return array<string, mixed>
257
     */
258 32
    private static function buildCacheHandler(array $options): array
259
    {
260 32
        $cachePath = (string) ($options['cachePath'] ?? null); // @phpstan-ignore-line
261
262 32
        if (is_dir($cachePath) && is_writable($cachePath)) {
263 2
            $handlerStack = HandlerStack::create();
264 2
            $handlerStack->push(middleware: new CacheMiddleware(cacheStrategy: new PrivateCacheStrategy(
265 2
                cache: new Psr6CacheStorage(cachePool: new FilesystemAdapter(namespace: 'numverify', defaultLifetime: 300, directory: $cachePath))
266 2
            )), name: 'cache');
267
268 2
            unset($options['cachePath']);
269 2
            $options += ['handler' => $handlerStack];
270
        }
271
272 32
        return $options;
273
    }
274
}
275