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

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