Client::getVersion()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace AcquiaCloudApi\Connector;
4
5
use Psr\Http\Message\ResponseInterface;
6
use GuzzleHttp\Exception\BadResponseException;
7
use AcquiaCloudApi\Exception\ApiErrorException;
8
use Psr\Http\Message\StreamInterface;
9
use RuntimeException;
10
11
/**
12
 * Class Client
13
 *
14
 * @package AcquiaCloudApi\CloudApi
15
 */
16
class Client implements ClientInterface
17
{
18
    /**
19
     * @var ConnectorInterface The API connector.
20
     */
21
    protected ConnectorInterface $connector;
22
23
    /**
24
     * @var array<string, mixed> Query strings to be applied to the request.
25
     */
26
    protected array $query = [];
27
28
    /**
29
     * @var array<string, mixed> Guzzle options to be applied to the request.
30
     */
31
    protected array $options = [];
32
33
    /**
34
     * @var array<string, mixed> Request options from each individual API call.
35
     */
36
    private array $requestOptions = [];
37
38
    /**
39
     * Client constructor.
40
     */
41
    final public function __construct(ConnectorInterface $connector)
42
    {
43
        $this->connector = $connector;
44
    }
45
46
    /**
47
     * Client factory method for instantiating.
48
     *
49
     * @return static
50
     */
51
    public static function factory(ConnectorInterface $connector): static
52
    {
53
        return new static(
54
            $connector
55
        );
56
    }
57
58
    /**
59
     * @inheritdoc
60
     */
61
    public function getVersion(): string
62
    {
63
        return self::VERSION;
64
    }
65
66
    /**
67
     * @inheritdoc
68
     */
69
    public function modifyOptions(): array
70
    {
71
        // Combine options set globally e.g. headers with options set by individual API calls e.g. form_params.
72
        $options = $this->options + $this->requestOptions;
73
74
        // This library can be standalone or as a dependency. Dependent libraries may also set their own user agent
75
        // which will make $options['headers']['User-Agent'] an array.
76
        // We need to array_unique() the array of User-Agent headers as multiple calls may include multiple of the same
77
        // header. We also use array_unshift() to place this library's user agent first to order to have it appear at
78
        // the beginning of log files.
79
        // As Guzzle joins arrays with a comma, we must implode with a space here to pass Guzzle a string.
80
        $userAgent = sprintf(
81
            "%s/%s (https://github.com/typhonius/acquia-php-sdk-v2)",
82
            'acquia-php-sdk-v2',
83
            $this->getVersion()
84
        );
85
        if (isset($options['headers']['User-Agent']) && is_array($options['headers']['User-Agent'])) {
86
            array_unshift($options['headers']['User-Agent'], $userAgent);
87
            $options['headers']['User-Agent'] = implode(' ', array_unique($options['headers']['User-Agent']));
88
        } else {
89
            $options['headers']['User-Agent'] = $userAgent;
90
        }
91
92
        $options['query'] = $this->query;
93
        if (!empty($options['query']['filter']) && is_array($options['query']['filter'])) {
94
            // Default to an OR filter to increase returned responses.
95
            $options['query']['filter'] = implode(',', $options['query']['filter']);
96
        }
97
98
        return $options;
99
    }
100
101
    /**
102
     * @inheritdoc
103
     * @throws \AcquiaCloudApi\Exception\ApiErrorException
104
     */
105
    public function request(string $verb, string $path, array $options = []): mixed
106
    {
107
        // Put options sent with API calls into a property, so they can be accessed
108
        // and therefore tested in tests.
109
        $this->requestOptions = $options;
110
111
        // Modify the options to combine options set as part of the API call as well
112
        // as those set by tools extending this library.
113
        $modifiedOptions = $this->modifyOptions();
114
115
        $response = $this->makeRequest($verb, $path, $modifiedOptions);
116
117
        return $this->processResponse($response);
118
    }
119
120
    /**
121
     * @inheritdoc
122
     */
123
    public function stream(string $verb, string $path, array $options = []): StreamInterface
124
    {
125
        // Put options sent with API calls into a property so they can be accessed
126
        // and therefore tested in tests.
127
        $this->requestOptions = $options;
128
129
        // Modify the options to combine options set as part of the API call as well
130
        // as those set by tools extending this library.
131
        $modifiedOptions = $this->modifyOptions();
132
133
        return $this->makeRequest($verb, $path, $modifiedOptions)->getBody();
134
    }
135
136
    /**
137
     * @inheritdoc
138
     */
139
    public function makeRequest(string $verb, string $path, array $options = []): ResponseInterface
140
    {
141
        try {
142
            $response = $this->connector->sendRequest($verb, $path, $options);
143
        } catch (BadResponseException $e) {
144
            $response = $e->getResponse();
145
        }
146
147
        return $response;
148
    }
149
150
    /**
151
     * @inheritdoc
152
     * @throws \AcquiaCloudApi\Exception\ApiErrorException
153
     * @throws \RuntimeException
154
     */
155
    public function processResponse(ResponseInterface $response): mixed
156
    {
157
        // Internal server errors return HTML instead of JSON, breaking our parsing.
158
        if ($response->getStatusCode() === 500) {
159
            throw new RuntimeException(
160
                'Cloud API internal server error. Status '
161
                . $response->getStatusCode()
162
                . '. Request ID '
163
                . $response->getHeaderLine('X-Request-Id')
164
            );
165
        }
166
        $body_json = $response->getBody();
167
        $body = json_decode($body_json, null, 512, JSON_THROW_ON_ERROR);
168
        // Internal server errors may also return a 200 response and empty body.
169
        if (is_null($body)) {
170
            throw new RuntimeException(
171
                'Response contained an empty body. Status '
172
                . $response->getStatusCode()
173
                . '. Request ID '
174
                . $response->getHeaderLine('X-Request-Id')
175
            );
176
        }
177
178
        if (property_exists($body, '_embedded') && property_exists($body->_embedded, 'items')) {
179
            return $body->_embedded->items;
180
        }
181
182
        if (property_exists($body, 'error') && property_exists($body, 'message')) {
183
            throw new ApiErrorException($body);
184
        }
185
186
        return $body;
187
    }
188
189
    /**
190
     * @inheritdoc
191
     */
192
    public function getQuery(): array
193
    {
194
        return $this->query;
195
    }
196
197
    /**
198
     * @inheritdoc
199
     */
200
    public function clearQuery(): void
201
    {
202
        $this->query = [];
203
    }
204
205
    /**
206
     * @inheritdoc
207
     */
208
    public function addQuery(string $name, int|string $value): void
209
    {
210
        $this->query = array_merge_recursive($this->query, [$name => $value]);
211
    }
212
213
    /**
214
     * @inheritdoc
215
     */
216
    public function getOptions(): array
217
    {
218
        return $this->options;
219
    }
220
221
    /**
222
     * @inheritdoc
223
     */
224
    public function clearOptions(): void
225
    {
226
        $this->options = [];
227
    }
228
229
    /**
230
     * @inheritdoc
231
     */
232
    public function addOption(string $name, mixed $value): void
233
    {
234
        $this->options = array_merge_recursive($this->options, [$name => $value]);
235
    }
236
}
237