Test Failed
Pull Request — master (#10)
by
unknown
14:44
created

Client::request()   B

Complexity

Conditions 9
Paths 13

Size

Total Lines 67
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 9.4027

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 9
eloc 39
c 1
b 0
f 1
nc 13
nop 4
dl 0
loc 67
ccs 34
cts 41
cp 0.8293
crap 9.4027
rs 7.7404

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Level23\Dynadot;
4
5
use Ramsey\Uuid\Uuid;
6
use GuzzleHttp\Psr7\Request;
7
use InvalidArgumentException;
8
use Level23\Dynadot\Dto\DtoInterface;
9
use GuzzleHttp\Client as GuzzleClient;
10
use GuzzleHttp\Exception\RequestException;
11
use Level23\Dynadot\Exception\ApiException;
12
use Level23\Dynadot\Dto\DomainListResult;
13
use Level23\Dynadot\Dto\ContactListResult;
14
use Level23\Dynadot\Dto\Contact;
15
use Level23\Dynadot\Dto\NameserverUpdateResult;
16
use Level23\Dynadot\Dto\RenewOptionResult;
17
use Level23\Dynadot\Dto\BulkSearchResult;
18
use Level23\Dynadot\Dto\SearchResult;
19
use Level23\Dynadot\Dto\DomainRegistrationResult;
20
use Level23\Dynadot\Dto\DomainRegistrationRequest;
21
use Level23\Dynadot\Exception\NetworkException;
22
23
class Client
24
{
25
    /** @var GuzzleClient */
26
    private GuzzleClient $http;
27
28
    /** @var string */
29
    private string $apiKey;
30
31
    /** @var string */
32
    private string $apiSecret;
33
34
    /** @var string */
35
    private string $apiVersion = 'v1';
36
37 5
    public function __construct(string $apiKey, string $apiSecret)
38
    {
39 5
        $this->apiKey    = $apiKey;
40 5
        $this->apiSecret = $apiSecret;
41 5
        $this->http      = new GuzzleClient([
42 5
            'base_uri' => 'https://api.dynadot.com/restful/' . $this->apiVersion . '/',
43 5
            'headers'  => [
44 5
                'Accept'       => 'application/json',
45 5
                'Content-Type' => 'application/json',
46 5
            ],
47 5
        ]);
48
    }
49
50
    /**
51
     * Retrieve detailed information about a single domain.
52
     *
53
     * @param string $domainName
54
     * @return DomainListResult
55
     * @throws ApiException
56
     * @throws NetworkException
57
     */
58 5
    public function getDomainInfo(string $domainName): DomainListResult
59
    {
60
        /** @var DomainListResult $result */
61 5
        $result = $this->request(
62 5
            'GET',
63 5
            "domains/{$domainName}",
64 5
            [],
65 5
            DomainListResult::class
66 5
        );
67
68 2
        return $result;
69
    }
70
71
    /**
72
     * Set nameservers for a domain.
73
     *
74
     * @param string $domainName
75
     * @param array<string> $nameservers
76
     * @throws ApiException
77
     * @throws NetworkException
78
     */
79
    public function setNameservers(string $domainName, array $nameservers): NameserverUpdateResult
80
    {
81
        /** @var NameserverUpdateResult $result */
82
        $result = $this->request(
83
            'PUT',
84
            "domains/{$domainName}/nameservers",
85
            [
86
                'nameservers_list' => $nameservers,
87
            ],
88
            NameserverUpdateResult::class
89
        );
90
91
        return $result;
92
    }
93
94
    /**
95
     * Retrieve contact information for a given contact ID.
96
     *
97
     * @param int $contactId
98
     * @return Contact
99
     * @throws ApiException
100
     * @throws NetworkException
101
     */
102
    public function getContactInfo(int $contactId): Contact
103
    {
104
        /** @var Contact $result */
105
        $result = $this->request(
106
            'GET',
107
            "contacts/{$contactId}",
108
            [],
109
            Contact::class
110
        );
111
112
        return $result;
113
    }
114
115
    /**
116
     * Retrieve a list of all contacts in your Dynadot account.
117
     *
118
     * @return ContactListResult
119
     * @throws ApiException
120
     * @throws NetworkException
121
     */
122
    public function getContactList(): ContactListResult
123
    {
124
        /** @var ContactListResult $result */
125
        $result = $this->request(
126
            'GET',
127
            "contacts",
128
            [],
129
            ContactListResult::class
130
        );
131
132
        return $result;
133
    }
134
135
    /**
136
     * Retrieve a list of all domains in your Dynadot account.
137
     *
138
     * This method returns detailed information about all domains including
139
     * domain names, expiration dates, registration dates, nameservers,
140
     * status information, and more.
141
     *
142
     * @return DomainListResult Contains an array of DomainInfo objects with detailed domain information
143
     * @throws ApiException When the API returns an error response
144
     * @throws NetworkException When there's a network communication error
145
     */
146
    public function getDomainList(): DomainListResult
147
    {
148
        /** @var DomainListResult $result */
149
        $result = $this->request(
150
            'GET',
151
            "domains",
152
            [],
153
            DomainListResult::class
154
        );
155
        return $result;
156
    }
157
158
    /**
159
     * Set the renew option for a domain.
160
     *
161
     * @param string $domain
162
     * @param string $renewOption
163
     * @return RenewOptionResult
164
     * @throws ApiException
165
     * @throws NetworkException
166
     */
167
    public function setRenewOption(string $domain, string $renewOption): RenewOptionResult
168
    {
169
        /** @var RenewOptionResult $result */
170
        $result = $this->request(
171
            'PUT',
172
            "domains/{$domain}/renew_option",
173
            ['renew_option' => $renewOption],
174
            RenewOptionResult::class
175
        );
176
177
        return $result;
178
    }
179
180
    /**
181
     * Search for multiple domains at once.
182
     *
183
     * @param array<string> $domains
184
     * @return BulkSearchResult
185
     * @throws ApiException
186
     * @throws NetworkException
187
     */
188
    public function bulkSearch(array $domains): BulkSearchResult
189
    {
190
        /** @var BulkSearchResult $result */
191
        $result = $this->request(
192
            'GET',
193
            "domains/bulk_search",
194
            ['domain_name_list' => implode(',', array_map('trim', $domains))],
195
            BulkSearchResult::class
196
        );
197
198
        return $result;
199
    }
200
201
    /**
202
     * Search for a domain.
203
     *
204
     * @param string $domain
205
     * @return SearchResult
206
     * @throws ApiException
207
     * @throws NetworkException
208
     */
209
    public function search(string $domain, bool $showPrice = false, string $currency = 'USD'): SearchResult
210
    {
211
        /** @var SearchResult $result */
212
        $result = $this->request(
213
            'GET',
214
            "domains/{$domain}/search",
215
            [
216
                'show_price' => $showPrice ? 'true' : 'false',
217
                'currency'   => $currency,
218
            ],
219
            SearchResult::class
220
        );
221
222
        return $result;
223
    }
224
225
    /**
226
     * Register a new domain.
227
     *
228
     * @param string $domainName
229
     * @param DomainRegistrationRequest $registrationData
230
     * @return DomainRegistrationResult
231
     * @throws ApiException
232
     * @throws NetworkException
233
     */
234
    public function registerDomain(string $domainName, DomainRegistrationRequest $registrationData): DomainRegistrationResult
235
    {
236
        /** @var DomainRegistrationResult $result */
237
        $result = $this->request(
238
            'POST',
239
            "domains/{$domainName}/register",
240
            $registrationData->jsonSerialize(),
241
            DomainRegistrationResult::class
242
        );
243
244
        return $result;
245
    }
246
247
    /**
248
     * Generic request helper that wraps Guzzle exceptions and hydrates DTOs.
249
     *
250
     * @param string $method
251
     * @param string $path
252
     * @param array<string, mixed>  $params
253
     * @param string $dtoClass
254
     *
255
     * @return DtoInterface
256
     * @throws ApiException
257
     * @throws NetworkException
258
     */
259 5
    private function request(string $method, string $path, array $params, string $dtoClass): DtoInterface
260
    {
261 5
        $requestId = Uuid::uuid4()->toString();
262
        
263
        // Prepare request options
264 5
        $options = [
265 5
            'headers' => [
266 5
                'Authorization' => 'Bearer ' . $this->apiKey,
267 5
                'X-Request-Id'  => $requestId,
268 5
                'Accept'       => 'application/json',
269 5
                'Content-Type' => 'application/json',
270 5
            ]
271 5
        ];
272
273
        // For GET requests, use params as query parameters
274 5
        if ($method === 'GET' && !empty($params)) {
275
            $options['query'] = $params;
276
            $payloadJson = '';
277
        } else {
278 5
            $payloadJson = json_encode($params, JSON_UNESCAPED_SLASHES);
279 5
            if ($payloadJson === false) {
280
                throw new InvalidArgumentException('Failed to encode request body as JSON');
281
            }
282 5
            $options['body'] = $payloadJson;
283
        }
284
285 5
        $stringToSign = implode("\n", [
286 5
            $this->apiKey,
287 5
            '/' . trim($path, '/'),
288 5
            $requestId,
289 5
            $payloadJson,
290 5
        ]);
291 5
        $signature = hash_hmac('sha256', $stringToSign, $this->apiSecret);
292 5
        $options['headers']['X-Signature'] = $signature;
293
294
        try {
295 5
            $response = $this->http->request($method, $path, $options);
296 3
        } catch (RequestException $e) {
297 3
            if ($e->getHandlerContext()['errno'] ?? null) {
298
                throw new NetworkException('Network error communicating with Dynadot API', 0, $e);
299
            }
300
301 3
            $response = $e->getResponse();
302 3
            if ($response === null) {
303 1
                throw new NetworkException('No response received from Dynadot API', 0, $e);
304
            }
305
306 2
            throw ApiException::fromResponse($e->getRequest(), $response, $e);
307
        }
308
309 2
        $data = json_decode($response->getBody()->getContents(), true);
310
311
        // check the status code in the json response and throw an exception if it's not 200
312 2
        if ($data['code'] >= 400) {
313
            // Create a request object for the exception
314
            $request = new Request($method, $path, $options['headers'], $options['body'] ?? null);
315
            throw ApiException::fromResponse($request, $response);
316
        }
317
318 2
        if (!is_a($dtoClass, DtoInterface::class, true)) {
319
            throw new InvalidArgumentException("$dtoClass must implement DtoInterface");
320
        }
321
322
        /** @var DtoInterface $dto */
323 2
        $dto = $dtoClass::fromArray($data['data'] ?? []);
324
325 2
        return $dto;
326
    }
327
}
328