Completed
Pull Request — master (#13)
by
unknown
02:14
created

ApiService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

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

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