Passed
Pull Request — master (#13)
by
unknown
26:12 queued 11:18
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
        foreach ($params as $key => $param) {
169
            if (is_array($param)) {
170 1
                foreach ($param as $k => $v) {
171 1
                    if (is_int($k)) {
172 1
                        $params[$key.'[]'][] = $v;
173 1
                    } else {
174
                        $params[$key.'['.$k.']'][] = $v;
175 1
                    }
176
                }
177 1
                unset($params[$key]);
178
            }
179
            if (false !== stripos($key, '_')) {
180
                $params[str_replace('_', '.', $key)] = $param;
181
                unset($params[$key]);
182
            }
183
        }
184
185
        return $params;
186
    }
187 13
188
    private function getBaseUri(): UriInterface
189
    {
190 13
        // Create a base uri from the API Schema
191 10
        if (null === $this->config['baseUri']) {
192 10
            $schemes = $this->schema->getSchemes();
193 10
            if (empty($schemes)) {
194 1
                throw new \LogicException('You need to provide at least on scheme in your API Schema');
195
            }
196
197 9
            $scheme = null;
198
            foreach ($this->schema->getSchemes() as $candidate) {
199 9
                // Always prefer https
200 8
                if ('https' === $candidate) {
201
                    $scheme = 'https';
202 9
                }
203 9
                if (null === $scheme && 'http' === $candidate) {
204
                    $scheme = 'http';
205
                }
206 9
            }
207 1
208
            if (null === $scheme) {
209
                throw new \RuntimeException('Cannot choose a proper scheme from the API Schema. Supported: https, http');
210 8
            }
211 8
212 1
            $host = $this->schema->getHost();
213
            if ('' === $host) {
214
                throw new \LogicException('The host in the API Schema should not be null');
215 7
            }
216
217 3
            return $this->uriFactory->createUri($scheme.'://'.$host);
218
        } else {
219
            return $this->uriFactory->createUri($this->config['baseUri']);
220
        }
221
    }
222
223
    private function getConfig(array $config): array
224 13
    {
225
        $config = array_merge(self::DEFAULT_CONFIG, $config);
226 13
        Assertion::boolean($config['returnResponse']);
227 13
        Assertion::boolean($config['validateRequest']);
228 13
        Assertion::boolean($config['validateResponse']);
229 13
        Assertion::nullOrString($config['baseUri']);
230 13
231
        return array_intersect_key($config, self::DEFAULT_CONFIG);
232 13
    }
233
234
    private function createRequestFromDefinition(RequestDefinition $definition, array $params): RequestInterface
235
    {
236
        $contentType = $definition->getContentTypes()[0] ?? 'application/json';
237
        $requestParameters = $definition->getRequestParameters();
238
        list($path, $query, $headers, $body, $formData) = $this->getDefaultValues($requestParameters);
239
        $headers = array_merge(
240
            $headers,
241
            ['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

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