Passed
Push — master ( f1352b...8ddea7 )
by Dane
01:59
created

Client::processResponse()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 19
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 32
rs 8.8333
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
     */
104
    public function request(string $verb, string $path, array $options = []): mixed
105
    {
106
        // Put options sent with API calls into a property, so they can be accessed
107
        // and therefore tested in tests.
108
        $this->requestOptions = $options;
109
110
        // Modify the options to combine options set as part of the API call as well
111
        // as those set by tools extending this library.
112
        $modifiedOptions = $this->modifyOptions();
113
114
        $response = $this->makeRequest($verb, $path, $modifiedOptions);
115
116
        return $this->processResponse($response);
117
    }
118
119
    /**
120
     * @inheritdoc
121
     */
122
    public function stream(string $verb, string $path, array $options = []): StreamInterface
123
    {
124
        // Put options sent with API calls into a property so they can be accessed
125
        // and therefore tested in tests.
126
        $this->requestOptions = $options;
127
128
        // Modify the options to combine options set as part of the API call as well
129
        // as those set by tools extending this library.
130
        $modifiedOptions = $this->modifyOptions();
131
132
        return $this->makeRequest($verb, $path, $modifiedOptions)->getBody();
133
    }
134
135
    /**
136
     * @inheritdoc
137
     */
138
    public function makeRequest(string $verb, string $path, array $options = []): ResponseInterface
139
    {
140
        try {
141
            $response = $this->connector->sendRequest($verb, $path, $options);
142
        } catch (BadResponseException $e) {
143
            $response = $e->getResponse();
144
        }
145
146
        return $response;
147
    }
148
149
    /**
150
     * @inheritdoc
151
     */
152
    public function processResponse(ResponseInterface $response): mixed
153
    {
154
        // Internal server errors return HTML instead of JSON, breaking our parsing.
155
        if ($response->getStatusCode() === 500) {
156
            throw new RuntimeException(
157
                'Cloud API internal server error. Status '
158
                . $response->getStatusCode()
159
                . '. Request ID '
160
                . $response->getHeaderLine('X-Request-Id')
161
            );
162
        }
163
        $body_json = $response->getBody();
164
        $body = json_decode($body_json, null, 512, JSON_THROW_ON_ERROR);
165
        // Internal server errors may also return a 200 response and empty body.
166
        if (is_null($body)) {
167
            throw new RuntimeException(
168
                'Response contained an empty body. Status '
169
                . $response->getStatusCode()
170
                . '. Request ID '
171
                . $response->getHeaderLine('X-Request-Id')
172
            );
173
        }
174
175
        if (property_exists($body, '_embedded') && property_exists($body->_embedded, 'items')) {
176
            return $body->_embedded->items;
177
        }
178
179
        if (property_exists($body, 'error') && property_exists($body, 'message')) {
180
            throw new ApiErrorException($body);
181
        }
182
183
        return $body;
184
    }
185
186
    /**
187
     * @inheritdoc
188
     */
189
    public function getQuery(): array
190
    {
191
        return $this->query;
192
    }
193
194
    /**
195
     * @inheritdoc
196
     */
197
    public function clearQuery(): void
198
    {
199
        $this->query = [];
200
    }
201
202
    /**
203
     * @inheritdoc
204
     */
205
    public function addQuery(string $name, int|string $value): void
206
    {
207
        $this->query = array_merge_recursive($this->query, [$name => $value]);
208
    }
209
210
    /**
211
     * @inheritdoc
212
     */
213
    public function getOptions(): array
214
    {
215
        return $this->options;
216
    }
217
218
    /**
219
     * @inheritdoc
220
     */
221
    public function clearOptions(): void
222
    {
223
        $this->options = [];
224
    }
225
226
    /**
227
     * @inheritdoc
228
     */
229
    public function addOption(string $name, mixed $value): void
230
    {
231
        $this->options = array_merge_recursive($this->options, [$name => $value]);
232
    }
233
}
234