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

Client::bulkSearch()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 6
c 1
b 0
f 1
nc 1
nop 1
dl 0
loc 11
ccs 0
cts 8
cp 0
crap 2
rs 10
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