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

ApiService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
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 11
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
    /**
94
     * ApiService constructor.
95 13
     *
96
     * @param UriFactory          $uriFactory
97
     * @param UriTemplate         $uriTemplate
98
     * @param HttpClient          $client
99
     * @param MessageFactory      $messageFactory
100
     * @param Schema              $schema
101
     * @param MessageValidator    $messageValidator
102
     * @param SerializerInterface $serializer
103
     * @param array               $config
104
     *
105 13
     * @throws \Assert\AssertionFailedException
106 13
     */
107 13
    public function __construct(UriFactory $uriFactory, UriTemplate $uriTemplate, HttpClient $client, MessageFactory $messageFactory, Schema $schema, MessageValidator $messageValidator, SerializerInterface $serializer, array $config = [])
108 13
    {
109 13
        $this->uriFactory = $uriFactory;
110 13
        $this->uriTemplate = $uriTemplate;
111 13
        $this->schema = $schema;
112 13
        $this->messageValidator = $messageValidator;
113 13
        $this->client = $client;
114 10
        $this->messageFactory = $messageFactory;
115
        $this->serializer = $serializer;
116
        $this->config = $this->getConfig($config);
117
        $this->baseUri = $this->getBaseUri();
118
    }
119
120
    /**
121
     * @param string $operationId The name of your operation as described in the API Schema
122
     * @param array  $params      An array of request parameters
123
     *
124 5
     * @throws ConstraintViolations
125
     * @throws \Http\Client\Exception
126 5
     *
127 5
     * @return ResourceInterface|ResponseInterface|array|object
128 5
     */
129
    public function call(string $operationId, array $params = [])
130 4
    {
131 4
        $requestDefinition = $this->schema->getRequestDefinition($operationId);
132
        $request = $this->createRequestFromDefinition($requestDefinition, $params);
133 3
        $this->validateRequest($request, $requestDefinition);
134 3
135 3
        $response = $this->client->sendRequest($request);
136 3
        $this->validateResponse($response, $requestDefinition);
137
138 3
        return $this->getDataFromResponse(
139
            $response,
140
            $requestDefinition->getResponseDefinition(
141 3
                $response->getStatusCode()
142
            ),
143
            $request
144
        );
145
    }
146
147
    /**
148
     * @param string $operationId
149
     * @param array  $params
150
     *
151
     * @throws \Exception
152 1
     *
153
     * @return Promise
154 1
     *
155
     */
156
    public function callAsync(string $operationId, array $params = []): Promise
157
    {
158
        if (!$this->client instanceof HttpAsyncClient) {
159
            throw new \RuntimeException(
160
                sprintf(
161
                    '"%s" does not support async request',
162
                    get_class($this->client)
163 1
                )
164 1
            );
165 1
        }
166
167 1
        $requestDefinition = $this->schema->getRequestDefinition($operationId);
168
        $request = $this->createRequestFromDefinition($requestDefinition, $params);
169
        $promise = $this->client->sendAsyncRequest($request);
170 1
171 1
        return $promise->then(
172 1
            function (ResponseInterface $response) use ($request, $requestDefinition) {
173 1
174
                return $this->getDataFromResponse(
175 1
                    $response,
176
                    $requestDefinition->getResponseDefinition(
177 1
                        $response->getStatusCode()
178
                    ),
179
                    $request
180
                );
181
            }
182
        );
183
    }
184
185
    public function getSchema(): Schema
186
    {
187 13
        return $this->schema;
188
    }
189
190 13
    /**
191 10
     * @return UriInterface
192 10
     */
193 10
    private function getBaseUri(): UriInterface
194 1
    {
195
        // Create a base uri from the API Schema
196
        if (null === $this->config['baseUri']) {
197 9
            $schemes = $this->schema->getSchemes();
198
            if (empty($schemes)) {
199 9
                throw new \LogicException('You need to provide at least on scheme in your API Schema');
200 8
            }
201
202 9
            $scheme = null;
203 9
            foreach ($this->schema->getSchemes() as $candidate) {
204
                // Always prefer https
205
                if ('https' === $candidate) {
206 9
                    $scheme = 'https';
207 1
                }
208
                if (null === $scheme && 'http' === $candidate) {
209
                    $scheme = 'http';
210 8
                }
211 8
            }
212 1
213
            if (null === $scheme) {
214
                throw new \RuntimeException('Cannot choose a proper scheme from the API Schema. Supported: https, http');
215 7
            }
216
217 3
            $host = $this->schema->getHost();
218
            if ('' === $host) {
219
                throw new \LogicException('The host in the API Schema should not be null');
220
            }
221
222
            return $this->uriFactory->createUri($scheme.'://'.$host);
223
        } else {
224 13
            return $this->uriFactory->createUri($this->config['baseUri']);
225
        }
226 13
    }
227 13
228 13
    /**
229 13
     * @param array $config
230 13
     *
231
     * @throws \Assert\AssertionFailedException
232 13
     *
233
     * @return array
234
     */
235
    private function getConfig(array $config): array
236
    {
237
        $config = array_merge(self::DEFAULT_CONFIG, $config);
238
        Assertion::boolean($config['returnResponse']);
239
        Assertion::boolean($config['validateRequest']);
240
        Assertion::boolean($config['validateResponse']);
241
        Assertion::nullOrString($config['baseUri']);
242
243
        return array_intersect_key($config, self::DEFAULT_CONFIG);
244
    }
245 6
246
    /**
247 6
     * @param RequestDefinition $definition
248 6
     * @param array             $params
249 6
     *
250 6
     * @return RequestInterface
251 6
     */
252 6
    private function createRequestFromDefinition(RequestDefinition $definition, array $params): RequestInterface
253
    {
254 6
        $contentType = $definition->getContentTypes()[0] ?? 'application/json';
255 2
        $requestParameters = $definition->getRequestParameters();
256 2
        list($path, $query, $headers, $body, $formData) = $this->getDefaultValues($requestParameters);
257
        $headers = array_merge(
258
            $headers,
259
            ['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

259
            ['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...
260 2
        );
261 2
262 1
        foreach ($params as $name => $value) {
263 1
            $requestParameter = $requestParameters->getByName($name);
264 2
            if (null === $requestParameter) {
265 2
                throw new \InvalidArgumentException(sprintf('%s is not a defined request parameter', $name));
266 2
            }
267 1
268
            switch ($requestParameter->getLocation()) {
269
                case 'path':
270 1
                    $path[$name] = $value;
271 2
                    break;
272
                case 'query':
273
                    $query[$name] = $value;
274
                    break;
275 6
                case 'header':
276 6
                    $headers[$name] = $value;
277 6
                    break;
278 6
                case 'body':
279 6
                    $body = $this->serializeRequestBody($value, $contentType);
280
                    break;
281
                case 'formData':
282 6
                    $formData[$name] = sprintf('%s=%s', $name, $value);
283
                    break;
284
            }
285
        }
286
287
        if (!empty($formData)) {
288
            $body = implode('&', $formData);
289
        }
290
291
        $request = $this->messageFactory->createRequest(
292
            $definition->getMethod(),
293
            $this->buildRequestUri($definition->getPathTemplate(), $path, $query),
294
            $headers,
295
            $body
296
        );
297
298
        return $request;
299
    }
300 6
301
    private function getDefaultValues(Parameters $requestParameters): array
302 6
    {
303 6
        $path = [];
304
        $query = [];
305 6
        $headers = [];
306
        $body = null;
307
        $formData = [];
308
309
        /** @var Parameter $parameter */
310
        foreach ($requestParameters->getIterator() as $name => $parameter) {
311
            if (isset($parameter->getSchema()->default)) {
312
                $value = $parameter->getSchema()->default;
313
                switch ($parameter->getLocation()) {
314 1
                    case 'path':
315
                        $path[$name] = $value;
316 1
                        break;
317 1
                    case 'query':
318 1
                        $query[$name] = $value;
319
                        break;
320
                    case 'header':
321
                        $headers[$name] = $value;
322
                        break;
323
                    case 'formData':
324
                        $formData[$name] = sprintf('%s=%s', $name, $value);
325
                        break;
326
                    case 'body':
327
                        $body = $this->serializeRequestBody($value, $contentType);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $contentType seems to be never defined.
Loading history...
328
                        break;
329
                }
330
            }
331
        }
332 4
333
        return [$path, $query, $headers, $body, $formData];
334
    }
335
336
337 4
    /**
338
     * @param string $pathTemplate    A template path
339
     * @param array  $pathParameters  Path parameters
340
     * @param array  $queryParameters Query parameters
341
     *
342 4
     * @return UriInterface
343 1
     */
344
    private function buildRequestUri(string $pathTemplate, array $pathParameters, array $queryParameters): UriInterface
345
    {
346 3
        $path = $this->uriTemplate->expand($pathTemplate, $pathParameters);
347 3
        $query = http_build_query($queryParameters);
348 3
349 3
        return $this->baseUri->withPath($path)->withQuery($query);
350
    }
351 3
352 3
    /**
353 3
     * @param array  $decodedBody
354
     * @param string $contentType
355
     *
356
     * @return string
357
     */
358
    private function serializeRequestBody(array $decodedBody, string $contentType): string
359
    {
360
        return $this->serializer->serialize(
361
            $decodedBody,
362
            DecoderUtils::extractFormatFromContentType($contentType)
363
        );
364
    }
365
366
    /**
367 5
     * @param ResponseInterface  $response
368
     * @param ResponseDefinition $definition
369 5
     * @param RequestInterface   $request
370 1
     *
371
     * @return ResourceInterface|ResponseInterface|array|object
372
     */
373 4
    private function getDataFromResponse(ResponseInterface $response, ResponseDefinition $definition, RequestInterface $request)
374 4
    {
375 1
        if (true === $this->config['returnResponse']) {
376 1
            return $response;
377
        }
378
379 3
        // @todo Find a better way to handle responses with a body definition
380
        if (!$definition->hasBodySchema()) {
381
            return new Item([], $request->getHeaders(), []);
382
        }
383
        $statusCode = $response->getStatusCode();
384
385
        return $this->serializer->deserialize(
386
            (string) $response->getBody(),
387
            $statusCode >= 400 && $statusCode <= 599 ? ErrorInterface::class : ResourceInterface::class,
388
            DecoderUtils::extractFormatFromContentType($response->getHeaderLine('Content-Type')),
389
            [
390 4
                'response' => $response,
391
                'responseDefinition' => $definition,
392 4
                'request' => $request,
393 2
            ]
394
        );
395
    }
396 2
397 2
    /**
398 1
     * @param RequestInterface  $request
399 1
     * @param RequestDefinition $definition
400
     *
401
     * @throws ConstraintViolations
402 1
     */
403
    private function validateRequest(RequestInterface $request, RequestDefinition $definition)
404
    {
405
        if (false === $this->config['validateRequest']) {
406
            return;
407
        }
408
409
        $this->messageValidator->validateRequest($request, $definition);
410
        if ($this->messageValidator->hasViolations()) {
411
            throw new RequestViolations(
412
                $this->messageValidator->getViolations()
413
            );
414
        }
415
    }
416
417
    /**
418
     * @param ResponseInterface $response
419
     * @param RequestDefinition $definition
420
     *
421
     * @throws ConstraintViolations
422
     */
423
    private function validateResponse(ResponseInterface $response, RequestDefinition $definition)
424
    {
425
        if (false === $this->config['validateResponse']) {
426
            return;
427
        }
428
429
        $this->messageValidator->validateResponse($response, $definition);
430
        if ($this->messageValidator->hasViolations()) {
431
            throw new ResponseViolations(
432
                $this->messageValidator->getViolations()
433
            );
434
        }
435
    }
436
}
437