Completed
Push — master ( 0ef30a...eb3939 )
by Kevin
03:35
created

Client::assertRequiredCommandOptions()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 18
rs 8.8571
cc 5
eloc 9
nc 4
nop 2
1
<?php
2
3
namespace PCextreme\Cloudstack;
4
5
use InvalidArgumentException;
6
use PCextreme\Cloudstack\Exception\ClientException;
7
use Psr\Http\Message\ResponseInterface;
8
use RuntimeException;
9
10
class Client extends AbstractClient
11
{
12
    /**
13
     * @var array
14
     */
15
    protected $apiList;
16
17
    /**
18
     * @var string
19
     */
20
    private $urlApi;
21
22
    /**
23
     * @var string
24
     */
25
    private $urlClient;
0 ignored issues
show
Unused Code introduced by
The property $urlClient is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
26
27
    /**
28
     * @var string
29
     */
30
    private $urlConsole;
0 ignored issues
show
Unused Code introduced by
The property $urlConsole is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
31
32
    /**
33
     * @var string
34
     */
35
    protected $apiKey;
36
37
    /**
38
     * @var string
39
     */
40
    protected $secretKey;
41
42
    /**
43
     * @var string
44
     */
45
    private $responseError = 'errortext';
46
47
    /**
48
     * @var string
49
     */
50
    private $responseCode = 'errorcode';
51
52
    /**
53
     * Constructs a new Cloudstack client instance.
54
     *
55
     * @param  array  $options
56
     *     An array of options to set on this client. Options include
57
     *     'apiList', 'urlApi', 'urlClient', 'urlConsole', 'apiKey',
58
     *     'secretKey', 'responseError' and 'responseCode'.
59
     * @param  array  $collaborators
60
     *     An array of collaborators that may be used to override
61
     *     this provider's default behavior. Collaborators include
62
     *     `requestFactory` and `httpClient`.
63
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
64
     */
65
    public function __construct(array $options = [], array $collaborators = [])
66
    {
67
        $this->assertRequiredOptions($options);
68
69
        $possible   = $this->getConfigurableOptions();
70
        $configured = array_intersect_key($options, array_flip($possible));
71
72
        foreach ($configured as $key => $value) {
73
            $this->$key = $value;
74
        }
75
76
        // Remove all options that are only used locally
77
        $options = array_diff_key($options, $configured);
78
79
        parent::__construct($options, $collaborators);
80
    }
81
82
    /**
83
     * Returns all options that can be configured.
84
     *
85
     * @return array
86
     */
87
    protected function getConfigurableOptions()
88
    {
89
        return array_merge($this->getRequiredOptions(), [
90
            'apiList',
91
            'urlClient',
92
            'urlConsole',
93
            'responseError',
94
            'responseCode',
95
        ]);
96
    }
97
98
    /**
99
     * Returns all options that are required.
100
     *
101
     * @return array
102
     */
103
    protected function getRequiredOptions()
104
    {
105
        return [
106
            'urlApi',
107
            'apiKey',
108
            'secretKey',
109
        ];
110
    }
111
112
    /**
113
     * Verifies that all required options have been passed.
114
     *
115
     * @param  array  $options
116
     * @return void
117
     * @throws InvalidArgumentException
118
     */
119
    private function assertRequiredOptions(array $options)
120
    {
121
        $missing = array_diff_key(array_flip($this->getRequiredOptions()), $options);
122
123
        if (! empty($missing)) {
124
            throw new InvalidArgumentException(
125
                'Required options not defined: ' . implode(', ', array_keys($missing))
126
            );
127
        }
128
    }
129
130
    public function command($command, array $options = [])
131
    {
132
        $this->assertRequiredCommandOptions($command, $options);
133
134
        $method  = $this->getCommandMethod($command);
135
        $url     = $this->getCommandUrl($command, $options);
136
        $request = $this->getRequest($method, $url, $options);
137
138
        return $this->getResponse($request);
139
    }
140
141
    /**
142
     * Verifies that all required options have been passed.
143
     *
144
     * @param  array $options
145
     * @return void
146
     * @throws RuntimeException
147
     * @throws InvalidArgumentException
148
     */
149
    private function assertRequiredCommandOptions($command, array $options = [])
150
    {
151
        $apiList = $this->getApiList();
152
153
        if (! array_key_exists($command, $apiList)) {
154
            throw new RuntimeException(
155
                "Call to unsupported API command [{$command}], this call is not present in the API list."
156
            );
157
        }
158
159
        foreach ($apiList[$command]['params'] as $key => $value) {
160
            if (! array_key_exists($key, $options) && (bool) $value['required']) {
161
                throw new InvalidArgumentException(
162
                    "Missing argument [{$key}] for command [{$command}] must be of type [{$value['type']}]."
163
                );
164
            }
165
        }
166
    }
167
168
    /**
169
     * Returns command method based on the command.
170
     *
171
     * @param  string  $command
172
     * @return array
173
     */
174
    public function getCommandMethod($command)
175
    {
176
        if (in_array($command, ['login', 'deployVirtualMachine'])) {
177
            return self::METHOD_POST;
178
        }
179
180
        return self::METHOD_GET;
181
    }
182
183
    /**
184
     * Builds the command URL's query string.
185
     *
186
     * @param  array  $params
187
     * @return string
188
     */
189
    public function getCommandQuery(array $params)
190
    {
191
        return $this->signCommandParameters($params);
192
    }
193
194
    /**
195
     * Builds the authorization URL.
196
     *
197
     * @param  string  $command
198
     * @param  array   $options
199
     * @return string
200
     */
201
    public function getCommandUrl($command, array $options = [])
202
    {
203
        $base   = $this->getBaseApiUrl();
204
        $params = $this->getCommandParameters($command, $options);
205
        $query  = $this->getCommandQuery($params);
206
207
        return $this->appendQuery($base, $query);
208
    }
209
210
    /**
211
     * Returns command parameters based on provided options.
212
     *
213
     * @param  string  $command
214
     * @param  array   $options
215
     * @return array
216
     */
217
    protected function getCommandParameters($command, array $options)
218
    {
219
        return array_merge($options, [
220
            'command'  => $command,
221
            'response' => 'json',
222
            'apikey'   => $this->getApiKey(),
223
        ]);
224
    }
225
226
    /**
227
     * Signs the command parameters.
228
     *
229
     * @param  array  $params
230
     * @return array
231
     */
232
    protected function signCommandParameters(array $params = [])
233
    {
234
        ksort($params);
235
236
        $query = $this->buildQueryString($params);
237
238
        $signature = rawurlencode(base64_encode(hash_hmac(
239
            'SHA1',
240
            strtolower($query),
241
            $this->getSecretKey(),
242
            true
243
        )));
244
245
        // To prevent the signature from being escaped we simply append
246
        // the signature to the previously build query.
247
        return $query . '&signature=' . $signature;
248
    }
249
250
    /**
251
     * Get Cloudstack Client API list.
252
     *
253
     * Tries to load the API list from the cache directory when
254
     * the 'apiList' on the class is empty.
255
     *
256
     * @return array
257
     * @throws RuntimeException
258
     */
259
    public function getApiList()
260
    {
261
        if (is_null($this->apiList)) {
262
            $path = __DIR__ . '/../cache/api_list.php';
263
264
            if (! file_exists($path)) {
265
                throw new RuntimeException(
266
                    "Cloudstack Client API list not found. This file needs to be generated before using the client."
267
                );
268
            }
269
270
            $this->apiList = require $path;
271
        }
272
273
        return $this->apiList;
274
    }
275
276
    /**
277
     * Set Cloudstack Client API list.
278
     *
279
     * @param  array  $list
0 ignored issues
show
Bug introduced by
There is no parameter named $list. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
280
     * @return void
281
     */
282
    public function setApiList(array $apiList)
283
    {
284
        $this->apiList = $apiList;
285
    }
286
287
    /**
288
     * Returns the base URL for API requests.
289
     *
290
     * @return string
291
     */
292
    public function getBaseApiUrl()
293
    {
294
        return $this->urlApi;
295
    }
296
297
    /**
298
     * Returns the API key.
299
     *
300
     * @return string
301
     */
302
    public function getApiKey()
303
    {
304
        return $this->apiKey;
305
    }
306
307
    /**
308
     * Returns the secret key.
309
     *
310
     * @return string
311
     */
312
    public function getSecretKey()
313
    {
314
        return $this->secretKey;
315
    }
316
317
    /**
318
     * Appends a query string to a URL.
319
     *
320
     * @param  string  $url
321
     * @param  string  $query
322
     * @return string
323
     */
324
    protected function appendQuery($url, $query)
325
    {
326
        $query = trim($query, '?&');
327
328
        if ($query) {
329
            return $url . '?' . $query;
330
        }
331
332
        return $url;
333
    }
334
335
    /**
336
     * Build a query string from an array.
337
     *
338
     * @param  array  $params
339
     * @return string
340
     */
341
    protected function buildQueryString(array $params)
342
    {
343
        return http_build_query($params, false, '&', PHP_QUERY_RFC3986);
344
    }
345
346
    /**
347
     * Checks a provider response for errors.
348
     *
349
     * @param  ResponseInterface  $response
350
     * @param  array|string       $data
351
     * @return void
352
     * @throws \PCextreme\Cloudstack\Exceptions\ClientException
353
     */
354
    protected function checkResponse(ResponseInterface $response, $data)
355
    {
356
        if (isset(reset($data)[$this->responseError])) {
357
            $error = reset($data)[$this->responseError];
358
            $code  = $this->responseCode ? reset($data)[$this->responseCode] : 0;
359
360
            throw new ClientException($error, $code, $data);
361
        }
362
    }
363
364
    /**
365
     * Handle dynamic method calls into the method.
366
     *
367
     * @param  string  $method
368
     * @param  array   $parameters
369
     * @return mixed
370
     */
371
    public function __call($method, $parameters)
372
    {
373
        array_unshift($parameters, $method);
374
375
        return call_user_func_array(array($this, 'command'), $parameters);
376
    }
377
}
378