Completed
Pull Request — master (#13)
by
unknown
03:54
created

ApiService   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 394
Duplicated Lines 0 %

Test Coverage

Coverage 97.44%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 158
c 1
b 1
f 0
dl 0
loc 394
ccs 152
cts 156
cp 0.9744
rs 9.0399
wmc 42

12 Methods

Rating   Name   Duplication   Size   Complexity  
A buildRequestUri() 0 6 1
A validateResponse() 0 10 3
A serializeRequestBody() 0 5 1
A getConfig() 0 9 1
B createRequestFromDefinition() 0 43 9
A callAsync() 0 24 2
B getDefaultValues() 0 33 8
A __construct() 0 11 1
A call() 0 15 1
B getBaseUri() 0 32 9
A validateRequest() 0 10 3
A getDataFromResponse() 0 19 3

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\Item;
18
use ElevenLabs\Api\Service\Resource\ResourceInterface;
19
use ElevenLabs\Api\Validator\MessageValidator;
20
use Http\Client\HttpAsyncClient;
21
use Http\Client\HttpClient;
22
use Http\Message\MessageFactory;
23
use Http\Message\UriFactory;
24
use Http\Promise\Promise;
25
use Psr\Http\Message\RequestInterface;
26
use Psr\Http\Message\ResponseInterface;
27
use Psr\Http\Message\UriInterface;
28
use Rize\UriTemplate;
29
use Symfony\Component\Serializer\SerializerInterface;
30
31
/**
32
 * Class ApiService.
33
 */
34
class ApiService
35
{
36
    const DEFAULT_CONFIG = [
37
        // override the scheme and host provided in the API schema by a baseUri (ex:https://domain.com)
38
        'baseUri' => null,
39
        // validate request
40
        'validateRequest' => true,
41
        // validate response
42
        'validateResponse' => false,
43
        // return response instead of a denormalized object
44
        'returnResponse' => false,
45
    ];
46
47
    /**
48
     * @var UriInterface
49
     */
50
    private $baseUri;
51
52
    /**
53
     * @var Schema
54
     */
55
    private $schema;
56
57
    /**
58
     * @var MessageValidator
59
     */
60
    private $messageValidator;
61
62
    /**
63
     * @var HttpAsyncClient|HttpClient
64
     */
65
    private $client;
66
67
    /**
68
     * @var MessageFactory
69
     */
70
    private $messageFactory;
71
72
    /**
73
     * @var UriTemplate
74
     */
75
    private $uriTemplate;
76
77
    /**
78
     * @var UriFactory
79
     */
80
    private $uriFactory;
81
82
    /**
83
     * @var SerializerInterface
84
     */
85
    private $serializer;
86
87
    /**
88
     * @var array
89
     */
90
    private $config;
91
92
    /**
93
     * ApiService constructor.
94
     *
95
     * @param UriFactory          $uriFactory
96
     * @param UriTemplate         $uriTemplate
97
     * @param HttpClient          $client
98
     * @param MessageFactory      $messageFactory
99
     * @param Schema              $schema
100
     * @param MessageValidator    $messageValidator
101
     * @param SerializerInterface $serializer
102
     * @param array               $config
103
     *
104
     * @throws \Assert\AssertionFailedException
105
     */
106 34
    public function __construct(UriFactory $uriFactory, UriTemplate $uriTemplate, HttpClient $client, MessageFactory $messageFactory, Schema $schema, MessageValidator $messageValidator, SerializerInterface $serializer, array $config = [])
107
    {
108 34
        $this->uriFactory = $uriFactory;
109 34
        $this->uriTemplate = $uriTemplate;
110 34
        $this->schema = $schema;
111 34
        $this->messageValidator = $messageValidator;
112 34
        $this->client = $client;
113 34
        $this->messageFactory = $messageFactory;
114 34
        $this->serializer = $serializer;
115 34
        $this->config = $this->getConfig($config);
116 30
        $this->baseUri = $this->getBaseUri();
117 27
    }
118
119
    /**
120
     * @param string $operationId The name of your operation as described in the API Schema
121
     * @param array  $params      An array of request parameters
122
     *
123
     * @throws ConstraintViolations
124
     * @throws \Http\Client\Exception
125
     *
126
     * @return ResourceInterface|ResponseInterface|array|object
127
     */
128 21
    public function call(string $operationId, array $params = [])
129
    {
130 21
        $requestDefinition = $this->schema->getRequestDefinition($operationId);
131 21
        $request = $this->createRequestFromDefinition($requestDefinition, $params);
132 20
        $this->validateRequest($request, $requestDefinition);
133
134 18
        $response = $this->client->sendRequest($request);
135 18
        $this->validateResponse($response, $requestDefinition);
136
137 16
        return $this->getDataFromResponse(
138 16
            $response,
139 16
            $requestDefinition->getResponseDefinition(
140 16
                $response->getStatusCode()
141
            ),
142 16
            $request
143
        );
144
    }
145
146
    /**
147
     * @param string $operationId
148
     * @param array  $params
149
     *
150
     * @throws \Exception
151
     *
152
     * @return Promise
153
     *
154
     */
155 1
    public function callAsync(string $operationId, array $params = []): Promise
156
    {
157 1
        if (!$this->client instanceof HttpAsyncClient) {
158
            throw new \RuntimeException(
159
                sprintf(
160
                    '"%s" does not support async request',
161
                    get_class($this->client)
162
                )
163
            );
164
        }
165
166 1
        $requestDefinition = $this->schema->getRequestDefinition($operationId);
167 1
        $request = $this->createRequestFromDefinition($requestDefinition, $params);
168 1
        $promise = $this->client->sendAsyncRequest($request);
169
170 1
        return $promise->then(
171
            function (ResponseInterface $response) use ($request, $requestDefinition) {
172
173 1
                return $this->getDataFromResponse(
174 1
                    $response,
175 1
                    $requestDefinition->getResponseDefinition(
176 1
                        $response->getStatusCode()
177
                    ),
178 1
                    $request
179
                );
180 1
            }
181
        );
182
    }
183
184
    /**
185
     * @return UriInterface
186
     */
187 30
    private function getBaseUri(): UriInterface
188
    {
189
        // Create a base uri from the API Schema
190 30
        if (null === $this->config['baseUri']) {
191 11
            $schemes = $this->schema->getSchemes();
192 11
            if (empty($schemes)) {
193 1
                throw new \LogicException('You need to provide at least on scheme in your API Schema');
194
            }
195
196 10
            $scheme = null;
197 10
            foreach ($this->schema->getSchemes() as $candidate) {
198
                // Always prefer https
199 10
                if ('https' === $candidate) {
200 8
                    $scheme = 'https';
201
                }
202 10
                if (null === $scheme && 'http' === $candidate) {
203 2
                    $scheme = 'http';
204
                }
205
            }
206
207 10
            if (null === $scheme) {
208 1
                throw new \RuntimeException('Cannot choose a proper scheme from the API Schema. Supported: https, http');
209
            }
210
211 9
            $host = $this->schema->getHost();
212 9
            if ('' === $host) {
213 1
                throw new \LogicException('The host in the API Schema should not be null');
214
            }
215
216 8
            return $this->uriFactory->createUri($scheme.'://'.$host);
217
        } else {
218 19
            return $this->uriFactory->createUri($this->config['baseUri']);
219
        }
220
    }
221
222
    /**
223
     * @param array $config
224
     *
225
     * @throws \Assert\AssertionFailedException
226
     *
227
     * @return array
228
     */
229 34
    private function getConfig(array $config): array
230
    {
231 34
        $config = array_merge(self::DEFAULT_CONFIG, $config);
232 34
        Assertion::boolean($config['returnResponse']);
233 33
        Assertion::boolean($config['validateRequest']);
234 32
        Assertion::boolean($config['validateResponse']);
235 31
        Assertion::nullOrString($config['baseUri']);
236
237 30
        return array_intersect_key($config, self::DEFAULT_CONFIG);
238
    }
239
240
    /**
241
     * @param RequestDefinition $definition
242
     * @param array             $params
243
     *
244
     * @return RequestInterface
245
     */
246 22
    private function createRequestFromDefinition(RequestDefinition $definition, array $params): RequestInterface
247
    {
248 22
        $contentType = $definition->getContentTypes()[0];
249 22
        $requestParameters = $definition->getRequestParameters();
250 22
        list($path, $query, $headers, $body, $formData) = $this->getDefaultValues($contentType, $requestParameters);
251
252 22
        foreach ($params as $name => $value) {
253 11
            $requestParameter = $requestParameters->getByName($name);
254 11
            if (null === $requestParameter) {
255 1
                throw new \InvalidArgumentException(sprintf('%s is not a defined request parameter', $name));
256
            }
257
258 10
            switch ($requestParameter->getLocation()) {
259 10
                case 'path':
260 2
                    $path[$name] = $value;
261 2
                    break;
262 9
                case 'query':
263 6
                    $query[$name] = $value;
264 6
                    break;
265 4
                case 'header':
266 1
                    $headers[$name] = $value;
267 1
                    break;
268 3
                case 'body':
269 2
                    $body = $this->serializeRequestBody($value, $contentType);
270 2
                    break;
271 1
                case 'formData':
272 1
                    $formData[$name] = sprintf('%s=%s', $name, $value);
273 1
                    break;
274
            }
275
        }
276
277 21
        if (!empty($formData)) {
278 2
            $body = implode('&', $formData);
279
        }
280
281 21
        $request = $this->messageFactory->createRequest(
282 21
            $definition->getMethod(),
283 21
            $this->buildRequestUri($definition->getPathTemplate(), $path, $query),
284 21
            $headers,
285 21
            $body
286
        );
287
288 21
        return $request;
289
    }
290
291
    /**
292
     * @param string     $contentType
293
     * @param Parameters $requestParameters
294
     *
295
     * @return array
296
     */
297 22
    private function getDefaultValues(string $contentType, Parameters $requestParameters): array
298
    {
299 22
        $path = [];
300 22
        $query = [];
301 22
        $headers = ['Content-Type' => $contentType];
302 22
        $body = null;
303 22
        $formData = [];
304
305
        /** @var Parameter $parameter */
306 22
        foreach ($requestParameters->getIterator() as $name => $parameter) {
307 10
            if (isset($parameter->getSchema()->default)) {
308 10
                $value = $parameter->getSchema()->default;
309 10
                switch ($parameter->getLocation()) {
310 10
                    case 'path':
311 6
                        $path[$name] = $value;
312 6
                        break;
313 9
                    case 'query':
314 6
                        $query[$name] = $value;
315 6
                        break;
316 8
                    case 'header':
317 6
                        $headers[$name] = $value;
318 6
                        break;
319 2
                    case 'formData':
320 1
                        $formData[$name] = sprintf('%s=%s', $name, $value);
321 1
                        break;
322 1
                    case 'body':
323 1
                        $body = $this->serializeRequestBody($value, $contentType);
324 1
                        break;
325
                }
326
            }
327
        }
328
329 22
        return [$path, $query, $headers, $body, $formData];
330
    }
331
332
333
    /**
334
     * @param string $pathTemplate    A template path
335
     * @param array  $pathParameters  Path parameters
336
     * @param array  $queryParameters Query parameters
337
     *
338
     * @return UriInterface
339
     */
340 21
    private function buildRequestUri(string $pathTemplate, array $pathParameters, array $queryParameters): UriInterface
341
    {
342 21
        $path = $this->uriTemplate->expand($pathTemplate, $pathParameters);
343 21
        $query = http_build_query($queryParameters);
344
345 21
        return $this->baseUri->withPath($path)->withQuery($query);
346
    }
347
348
    /**
349
     * @param array  $decodedBody
350
     * @param string $contentType
351
     *
352
     * @return string
353
     */
354 3
    private function serializeRequestBody(array $decodedBody, string $contentType): string
355
    {
356 3
        return $this->serializer->serialize(
357 3
            $decodedBody,
358 3
            DecoderUtils::extractFormatFromContentType($contentType)
359
        );
360
    }
361
362
    /**
363
     * @param ResponseInterface  $response
364
     * @param ResponseDefinition $definition
365
     * @param RequestInterface   $request
366
     *
367
     * @return ResourceInterface|ResponseInterface|array|object
368
     */
369 17
    private function getDataFromResponse(ResponseInterface $response, ResponseDefinition $definition, RequestInterface $request)
370
    {
371 17
        if (true === $this->config['returnResponse']) {
372 1
            return $response;
373
        }
374
375
        // @todo Find a better way to handle responses with a body definition
376 16
        if (!$definition->hasBodySchema()) {
377 11
            return new Item([], $request->getHeaders(), []);
378
        }
379
380 5
        return $this->serializer->deserialize(
381 5
            (string) $response->getBody(),
382 5
            ResourceInterface::class,
383 5
            DecoderUtils::extractFormatFromContentType($response->getHeaderLine('Content-Type')),
384
            [
385 5
                'response' => $response,
386 5
                'responseDefinition' => $definition,
387 5
                'request' => $request,
388
            ]
389
        );
390
    }
391
392
    /**
393
     * @param RequestInterface  $request
394
     * @param RequestDefinition $definition
395
     *
396
     * @throws ConstraintViolations
397
     */
398 20
    private function validateRequest(RequestInterface $request, RequestDefinition $definition)
399
    {
400 20
        if (false === $this->config['validateRequest']) {
401 2
            return;
402
        }
403
404 18
        $this->messageValidator->validateRequest($request, $definition);
405 18
        if ($this->messageValidator->hasViolations()) {
406 2
            throw new RequestViolations(
407 2
                $this->messageValidator->getViolations()
408
            );
409
        }
410 16
    }
411
412
    /**
413
     * @param ResponseInterface $response
414
     * @param RequestDefinition $definition
415
     *
416
     * @throws ConstraintViolations
417
     */
418 18
    private function validateResponse(ResponseInterface $response, RequestDefinition $definition)
419
    {
420 18
        if (false === $this->config['validateResponse']) {
421 10
            return;
422
        }
423
424 8
        $this->messageValidator->validateResponse($response, $definition);
425 8
        if ($this->messageValidator->hasViolations()) {
426 2
            throw new ResponseViolations(
427 2
                $this->messageValidator->getViolations()
428
            );
429
        }
430 6
    }
431
}
432