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

ApiService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 9
c 0
b 0
f 0
nc 1
nop 8
dl 0
loc 19
ccs 8
cts 8
cp 1
crap 1
rs 9.9666

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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