Passed
Pull Request — master (#13)
by
unknown
13:38
created

ApiService   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 367
Duplicated Lines 0 %

Test Coverage

Coverage 93.75%

Importance

Changes 5
Bugs 1 Features 0
Metric Value
eloc 181
dl 0
loc 367
ccs 120
cts 128
cp 0.9375
rs 5.04
c 5
b 1
f 0
wmc 57

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getSchema() 0 3 1
A callAsync() 0 24 2
A __construct() 0 19 1
A call() 0 15 1
A buildRequestUri() 0 6 1
A validateResponse() 0 10 3
A serializeRequestBody() 0 5 1
A getConfig() 0 9 1
B createRequestFromDefinition() 0 47 9
C getDefaultValues() 0 42 12
B getBaseUri() 0 32 9
A validateRequest() 0 10 3
A buildQuery() 0 12 4
A transformArray() 0 12 3
A getDataFromResponse() 0 25 6

How to fix   Complexity   

Complex Class

Complex classes like ApiService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ApiService, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace ElevenLabs\Api\Service;
6
7
use Assert\Assertion;
8
use ElevenLabs\Api\Decoder\DecoderUtils;
9
use ElevenLabs\Api\Definition\Parameter;
10
use ElevenLabs\Api\Definition\Parameters;
11
use ElevenLabs\Api\Definition\RequestDefinition;
12
use ElevenLabs\Api\Definition\ResponseDefinition;
13
use ElevenLabs\Api\Schema;
14
use ElevenLabs\Api\Service\Exception\ConstraintViolations;
15
use ElevenLabs\Api\Service\Exception\RequestViolations;
16
use ElevenLabs\Api\Service\Exception\ResponseViolations;
17
use ElevenLabs\Api\Service\Resource\ErrorInterface;
18
use ElevenLabs\Api\Service\Resource\Item;
19
use ElevenLabs\Api\Service\Resource\ResourceInterface;
20
use ElevenLabs\Api\Validator\MessageValidator;
21
use Http\Client\HttpAsyncClient;
22
use Http\Client\HttpClient;
23
use Http\Message\MessageFactory;
24
use Http\Message\UriFactory;
25
use Http\Promise\Promise;
26
use Psr\Http\Message\RequestInterface;
27
use Psr\Http\Message\ResponseInterface;
28
use Psr\Http\Message\UriInterface;
29
use Rize\UriTemplate;
30
use Symfony\Component\Serializer\SerializerInterface;
31
32
/**
33
 * Class ApiService.
34
 */
35
class ApiService
36
{
37
    const DEFAULT_CONFIG = [
38
        // override the scheme and host provided in the API schema by a baseUri (ex:https://domain.com)
39
        'baseUri' => null,
40
        // validate request
41
        'validateRequest' => true,
42
        // validate response
43
        'validateResponse' => false,
44
        // return response instead of a denormalized object
45
        'returnResponse' => false,
46
    ];
47
48
    /**
49
     * @var UriInterface
50
     */
51
    private $baseUri;
52
53
    /**
54
     * @var Schema
55
     */
56
    private $schema;
57
58
    /**
59
     * @var MessageValidator
60
     */
61
    private $messageValidator;
62
63
    /**
64
     * @var HttpAsyncClient|HttpClient
65
     */
66
    private $client;
67
68
    /**
69
     * @var MessageFactory
70
     */
71
    private $messageFactory;
72
73
    /**
74
     * @var UriTemplate
75
     */
76
    private $uriTemplate;
77
78
    /**
79
     * @var UriFactory
80
     */
81
    private $uriFactory;
82
83
    /**
84
     * @var SerializerInterface
85
     */
86
    private $serializer;
87
88
    /**
89
     * @var array
90
     */
91
    private $config;
92
93
    public function __construct(
94
        UriFactory $uriFactory,
95 13
        UriTemplate $uriTemplate,
96
        HttpClient $client,
97
        MessageFactory $messageFactory,
98
        Schema $schema,
99
        MessageValidator $messageValidator,
100
        SerializerInterface $serializer,
101
        array $config = []
102
    ) {
103
        $this->uriFactory = $uriFactory;
104
        $this->uriTemplate = $uriTemplate;
105 13
        $this->schema = $schema;
106 13
        $this->messageValidator = $messageValidator;
107 13
        $this->client = $client;
108 13
        $this->messageFactory = $messageFactory;
109 13
        $this->serializer = $serializer;
110 13
        $this->config = $this->getConfig($config);
111 13
        $this->baseUri = $this->getBaseUri();
112 13
    }
113 13
114 10
    public function call(string $operationId, array $params = [])
115
    {
116
        $requestDefinition = $this->schema->getRequestDefinition($operationId);
117
        $request = $this->createRequestFromDefinition($requestDefinition, $params);
118
        $this->validateRequest($request, $requestDefinition);
119
120
        $response = $this->client->sendRequest($request);
121
        $this->validateResponse($response, $requestDefinition);
122
123
        return $this->getDataFromResponse(
124 5
            $response,
125
            $requestDefinition->getResponseDefinition(
126 5
                $response->getStatusCode()
127 5
            ),
128 5
            $request
129
        );
130 4
    }
131 4
132
    public function callAsync(string $operationId, array $params = []): Promise
133 3
    {
134 3
        if (!$this->client instanceof HttpAsyncClient) {
135 3
            throw new \RuntimeException(
136 3
                sprintf(
137
                    '"%s" does not support async request',
138 3
                    get_class($this->client)
139
                )
140
            );
141 3
        }
142
143
        $requestDefinition = $this->schema->getRequestDefinition($operationId);
144
        $request = $this->createRequestFromDefinition($requestDefinition, $params);
145
        $promise = $this->client->sendAsyncRequest($request);
146
147
        return $promise->then(
148
            function (ResponseInterface $response) use ($request, $requestDefinition) {
149
150
                return $this->getDataFromResponse(
151
                    $response,
152 1
                    $requestDefinition->getResponseDefinition(
153
                        $response->getStatusCode()
154 1
                    ),
155
                    $request
156
                );
157
            }
158
        );
159
    }
160
161
    public function getSchema(): Schema
162
    {
163 1
        return $this->schema;
164 1
    }
165 1
166
    public static function buildQuery(array $params): array
167 1
    {
168
        $queryParameters = [];
169
        foreach ($params as $key => $item) {
170 1
            $queryParameters[str_replace('_', '.', $key)] = $item;
171 1
172 1
            if (\is_array($item) && self::transformArray($queryParameters, $key, $item)) {
173 1
                unset($queryParameters[$key]);
174
            }
175 1
        }
176
177 1
        return $queryParameters;
178
    }
179
180
    private static function transformArray(&$queryParameters, $key, $item)
181
    {
182
        foreach ($item as $property => $value) {
183
            // if array like ["value 1", "value 2"], do not transform
184
            if (\is_int($property)) {
185
                return false;
186
            }
187 13
            // array like ["key" => "value"], transform
188
            $queryParameters[$key.'['.$property.']'] = $value;
189
        }
190 13
191 10
        return true;
192 10
    }
193 10
194 1
    private function getBaseUri(): UriInterface
195
    {
196
        // Create a base uri from the API Schema
197 9
        if (null === $this->config['baseUri']) {
198
            $schemes = $this->schema->getSchemes();
199 9
            if (empty($schemes)) {
200 8
                throw new \LogicException('You need to provide at least on scheme in your API Schema');
201
            }
202 9
203 9
            $scheme = null;
204
            foreach ($this->schema->getSchemes() as $candidate) {
205
                // Always prefer https
206 9
                if ('https' === $candidate) {
207 1
                    $scheme = 'https';
208
                }
209
                if (null === $scheme && 'http' === $candidate) {
210 8
                    $scheme = 'http';
211 8
                }
212 1
            }
213
214
            if (null === $scheme) {
215 7
                throw new \RuntimeException('Cannot choose a proper scheme from the API Schema. Supported: https, http');
216
            }
217 3
218
            $host = $this->schema->getHost();
219
            if ('' === $host) {
220
                throw new \LogicException('The host in the API Schema should not be null');
221
            }
222
223
            return $this->uriFactory->createUri($scheme.'://'.$host);
224 13
        } else {
225
            return $this->uriFactory->createUri($this->config['baseUri']);
226 13
        }
227 13
    }
228 13
229 13
    private function getConfig(array $config): array
230 13
    {
231
        $config = array_merge(self::DEFAULT_CONFIG, $config);
232 13
        Assertion::boolean($config['returnResponse']);
233
        Assertion::boolean($config['validateRequest']);
234
        Assertion::boolean($config['validateResponse']);
235
        Assertion::nullOrString($config['baseUri']);
236
237
        return array_intersect_key($config, self::DEFAULT_CONFIG);
238
    }
239
240
    private function createRequestFromDefinition(RequestDefinition $definition, array $params): RequestInterface
241
    {
242
        $contentType = $definition->getContentTypes()[0] ?? 'application/json';
243
        $requestParameters = $definition->getRequestParameters();
244
        list($path, $query, $headers, $body, $formData) = $this->getDefaultValues($requestParameters);
245 6
        $headers = array_merge(
246
            $headers,
247 6
            ['Content-Type' => $contentType, 'Accept' => $definition->getAccepts()[0] ?? 'application/json']
0 ignored issues
show
Bug introduced by
The method getAccepts() does not exist on ElevenLabs\Api\Definition\RequestDefinition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

247
            ['Content-Type' => $contentType, 'Accept' => $definition->/** @scrutinizer ignore-call */ getAccepts()[0] ?? 'application/json']

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
248 6
        );
249 6
250 6
        foreach ($params as $name => $value) {
251 6
            $requestParameter = $requestParameters->getByName($name);
252 6
            if (null === $requestParameter) {
253
                throw new \InvalidArgumentException(sprintf('%s is not a defined request parameter for operationId %s', $name, $definition->getOperationId()));
254 6
            }
255 2
256 2
            switch ($requestParameter->getLocation()) {
257
                case 'path':
258
                    $path[$name] = $value;
259
                    break;
260 2
                case 'query':
261 2
                    $query[$name] = $value;
262 1
                    break;
263 1
                case 'header':
264 2
                    $headers[$name] = $value;
265 2
                    break;
266 2
                case 'body':
267 1
                    $body = $this->serializeRequestBody(array_merge($body ?? [], $value), $contentType);
268
                    break;
269
                case 'formData':
270 1
                    $formData[$name] = sprintf('%s=%s', $name, $value);
271 2
                    break;
272
            }
273
        }
274
275 6
        if (!empty($formData)) {
276 6
            $body = implode('&', $formData);
277 6
        }
278 6
279 6
        $request = $this->messageFactory->createRequest(
280
            $definition->getMethod(),
281
            $this->buildRequestUri($definition->getPathTemplate(), $path, $query),
282 6
            $headers,
283
            $body
284
        );
285
286
        return $request;
287
    }
288
289
    private function getDefaultValues(Parameters $requestParameters): array
290
    {
291
        $path = [];
292
        $query = [];
293
        $headers = [];
294
        $body = null;
295
        $formData = [];
296
297
        /** @var Parameter $parameter */
298
        foreach ($requestParameters->getIterator() as $name => $parameter) {
299
            switch ($parameter->getLocation()) {
300 6
                case 'path':
301
                    if (!empty($parameter->getSchema()->default)) {
302 6
                        $path[$name] = $parameter->getSchema()->default;
303 6
                    }
304
                    break;
305 6
                case 'query':
306
                    if (!empty($parameter->getSchema()->default)) {
307
                        $query[$name] = $parameter->getSchema()->default;
308
                    }
309
                    break;
310
                case 'header':
311
                    if (!empty($parameter->getSchema()->default)) {
312
                        $headers[$name] = $parameter->getSchema()->default;
313
                    }
314 1
                    break;
315
                case 'formData':
316 1
                    if (!empty($parameter->getSchema()->default)) {
317 1
                        $formData[$name] = sprintf('%s=%s', $name, $parameter->getSchema()->default);
318 1
                    }
319
                    break;
320
                case 'body':
321
                    if (!empty($parameter->getSchema()->properties)) {
322
                        $body = array_filter(array_map(function (array $params) {
323
                            return $params['default'] ?? null;
324
                        }, json_decode(json_encode($parameter->getSchema()->properties), true)));
325
                    }
326
                    break;
327
            }
328
        }
329
330
        return [$path, $query, $headers, $body, $formData];
331
    }
332 4
333
    private function buildRequestUri(string $pathTemplate, array $pathParameters, array $queryParameters): UriInterface
334
    {
335
        $path = $this->uriTemplate->expand($pathTemplate, $pathParameters);
336
        $query = http_build_query($queryParameters);
337 4
338
        return $this->baseUri->withPath($path)->withQuery($query);
339
    }
340
341
    private function serializeRequestBody(array $decodedBody, string $contentType): string
342 4
    {
343 1
        return $this->serializer->serialize(
344
            $decodedBody,
345
            DecoderUtils::extractFormatFromContentType($contentType)
346 3
        );
347 3
    }
348 3
349 3
    private function getDataFromResponse(ResponseInterface $response, ResponseDefinition $definition, RequestInterface $request)
350
    {
351 3
        if (true === $this->config['returnResponse']) {
352 3
            return $response;
353 3
        }
354
355
        // @todo Find a better way to handle responses with a body definition
356
        if (!$definition->hasBodySchema()) {
357
            return new Item([], $request->getHeaders(), []);
358
        }
359
360
        if (empty($response->getHeaderLine('Content-Type'))) {
361
            return new Item([], $request->getHeaders(), []);
362
        }
363
364
        $statusCode = $response->getStatusCode();
365
366
        return $this->serializer->deserialize(
367 5
            (string) $response->getBody(),
368
            $statusCode >= 400 && $statusCode <= 599 ? ErrorInterface::class : ResourceInterface::class,
369 5
            DecoderUtils::extractFormatFromContentType($response->getHeaderLine('Content-Type')),
370 1
            [
371
                'response' => $response,
372
                'responseDefinition' => $definition,
373 4
                'request' => $request,
374 4
            ]
375 1
        );
376 1
    }
377
378
    private function validateRequest(RequestInterface $request, RequestDefinition $definition)
379 3
    {
380
        if (false === $this->config['validateRequest']) {
381
            return;
382
        }
383
384
        $this->messageValidator->validateRequest($request, $definition);
385
        if ($this->messageValidator->hasViolations()) {
386
            throw new RequestViolations(
387
                $this->messageValidator->getViolations()
388
            );
389
        }
390 4
    }
391
392 4
    private function validateResponse(ResponseInterface $response, RequestDefinition $definition)
393 2
    {
394
        if (false === $this->config['validateResponse']) {
395
            return;
396 2
        }
397 2
398 1
        $this->messageValidator->validateResponse($response, $definition);
399 1
        if ($this->messageValidator->hasViolations()) {
400
            throw new ResponseViolations(
401
                $this->messageValidator->getViolations()
402 1
            );
403
        }
404
    }
405
}
406