Completed
Pull Request — master (#13)
by
unknown
04: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
namespace ElevenLabs\Api\Service;
4
5
use Assert\Assertion;
6
use ElevenLabs\Api\Decoder\DecoderUtils;
7
use ElevenLabs\Api\Definition\Parameter;
8
use ElevenLabs\Api\Definition\Parameters;
9
use ElevenLabs\Api\Definition\RequestDefinition;
10
use ElevenLabs\Api\Definition\ResponseDefinition;
11
use ElevenLabs\Api\Schema;
12
use ElevenLabs\Api\Service\Exception\ConstraintViolations;
13
use ElevenLabs\Api\Service\Exception\RequestViolations;
14
use ElevenLabs\Api\Service\Exception\ResponseViolations;
15
use ElevenLabs\Api\Service\Resource\Item;
16
use ElevenLabs\Api\Service\Resource\Resource;
17
use ElevenLabs\Api\Validator\MessageValidator;
18
use Http\Client\HttpAsyncClient;
19
use Http\Client\HttpClient;
20
use Http\Message\MessageFactory;
21
use Http\Message\UriFactory;
22
use Http\Promise\Promise;
23
use Psr\Http\Message\RequestInterface;
24
use Psr\Http\Message\ResponseInterface;
25
use Psr\Http\Message\UriInterface;
26
use Rize\UriTemplate;
27
use Symfony\Component\Serializer\SerializerInterface;
28
29
/**
30
 * A client that provide API service commands (pretty much like Guzzle)
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     The BaseUri of your API
94
     * @param UriTemplate         $uriTemplate    Used to expand Uri pattern in the API definition
95
     * @param HttpClient          $client         An HTTP 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
     * Make a synchronous call to the API
127
     *
128
     * @param string $operationId The name of your operation as described in the API Schema
129
     * @param array  $params      An array of request parameters
130
     *
131
     * @return Resource|mixed
132
     *
133
     * @throws ConstraintViolations
134
     * @throws \Http\Client\Exception
135
     */
136 21
    public function call($operationId, array $params = [])
137
    {
138 21
        $requestDefinition = $this->schema->getRequestDefinition($operationId);
139 21
        $request = $this->createRequestFromDefinition($requestDefinition, $params);
140 20
        $this->validateRequest($request, $requestDefinition);
141
142 18
        $response = $this->client->sendRequest($request);
143 18
        $this->validateResponse($response, $requestDefinition);
144
145 16
        $data = $this->getDataFromResponse(
146 16
            $response,
147 16
            $requestDefinition->getResponseDefinition(
148 16
                $response->getStatusCode()
149
            ),
150 16
            $request
151
        );
152
153 16
        return $data;
154
    }
155
156
    /**
157
     * Make an asynchronous call to the API
158
     *
159
     * @param string $operationId
160
     * @param array  $params
161
     *
162
     * @return Promise
163
     *
164
     * @throws \Exception
165
     */
166 1
    public function callAsync($operationId, array $params = [])
167
    {
168 1
        if (! $this->client instanceof HttpAsyncClient) {
169
            throw new \RuntimeException(
170
                sprintf(
171
                    '"%s" does not support async request',
172
                    get_class($this->client)
173
                )
174
            );
175
        }
176
177 1
        $requestDefinition = $this->schema->getRequestDefinition($operationId);
178 1
        $request = $this->createRequestFromDefinition($requestDefinition, $params);
179 1
        $promise = $this->client->sendAsyncRequest($request);
180
181 1
        return $promise->then(
182
            function (ResponseInterface $response) use ($request, $requestDefinition) {
183
184 1
                return $this->getDataFromResponse(
185 1
                    $response,
186 1
                    $requestDefinition->getResponseDefinition(
187 1
                        $response->getStatusCode()
188
                    ),
189 1
                    $request
190
                );
191 1
            }
192
        );
193
    }
194
195
    /**
196
     * Configure the base uri from using the API Schema if no baseUri is provided
197
     * Or from the baseUri config if provided
198
     *
199
     * @return UriInterface
200
     */
201 30
    private function getBaseUri()
202
    {
203
        // Create a base uri from the API Schema
204 30
        if ($this->config['baseUri'] === null) {
205 11
            $scheme = null;
206 11
            $schemes = $this->schema->getSchemes();
207 11
            if (null === $schemes) {
0 ignored issues
show
introduced by
The condition null === $schemes is always false.
Loading history...
208 1
                throw new \LogicException('You need to provide at least on scheme in your API Schema');
209
            }
210
211 10
            foreach ($this->schema->getSchemes() as $candidate) {
212
                // Always prefer https
213 10
                if ('https' === $candidate) {
214 8
                    $scheme = 'https';
215
                }
216 10
                if (null === $scheme && 'http' === $candidate) {
217 2
                    $scheme = 'http';
218
                }
219
            }
220 10
            if (null === $scheme) {
221 1
                throw new \RuntimeException('Cannot choose a proper scheme from the API Schema. Supported: https, http');
222
            }
223
224 9
            $host = $this->schema->getHost();
225 9
            if (null === $host) {
0 ignored issues
show
introduced by
The condition null === $host is always false.
Loading history...
226 1
                throw new \LogicException('The host in the API Schema should not be null');
227
            }
228
229 8
            return $this->uriFactory->createUri($scheme.'://'.$host);
230
        } else {
231 19
            return $this->uriFactory->createUri($this->config['baseUri']);
232
        }
233
    }
234
235
    /**
236
     * @param array $config
237
     *
238
     * @return array
239
     *
240
     * @throws \Assert\AssertionFailedException
241
     */
242 34
    private function getConfig(array $config)
243
    {
244 34
        $config = array_merge(self::DEFAULT_CONFIG, $config);
245 34
        Assertion::boolean($config['returnResponse']);
246 33
        Assertion::boolean($config['validateRequest']);
247 32
        Assertion::boolean($config['validateResponse']);
248 31
        Assertion::nullOrString($config['baseUri']);
249
250 30
        return array_intersect_key($config, self::DEFAULT_CONFIG);
251
    }
252
253
    /**
254
     * Create an PSR-7 Request from the API Schema
255
     *
256
     * @param RequestDefinition $definition
257
     * @param array             $params     An array of parameters
258
     *
259
     * @return RequestInterface
260
     */
261 22
    private function createRequestFromDefinition(RequestDefinition $definition, array $params)
262
    {
263 22
        $contentType = $definition->getContentTypes()[0];
264 22
        $requestParameters = $definition->getRequestParameters();
265 22
        list($path, $query, $headers, $body, $formData) = $this->getDefaultValues($contentType, $requestParameters);
266
267 22
        foreach ($params as $name => $value) {
268 11
            $requestParameter = $requestParameters->getByName($name);
269 11
            if (null === $requestParameter) {
270 1
                throw new \InvalidArgumentException(sprintf('%s is not a defined request parameter', $name));
271
            }
272
273 10
            switch ($requestParameter->getLocation()) {
274 10
                case 'path':
275 2
                    $path[$name] = $value;
276 2
                    break;
277 9
                case 'query':
278 6
                    $query[$name] = $value;
279 6
                    break;
280 4
                case 'header':
281 1
                    $headers[$name] = $value;
282 1
                    break;
283 3
                case 'body':
284 2
                    $body = $this->serializeRequestBody($value, $contentType);
285 2
                    break;
286 1
                case 'formData':
287 1
                    $formData[$name] = sprintf("%s=%s", $name, $value);
288 1
                    break;
289
            }
290
        }
291
292 21
        if (!empty($formData)) {
293 2
            $body = implode("&", $formData);
294
        }
295
296 21
        $request = $this->messageFactory->createRequest(
297 21
            $definition->getMethod(),
298 21
            $this->buildRequestUri($definition->getPathTemplate(), $path, $query),
299 21
            $headers,
300 21
            $body
301
        );
302
303 21
        return $request;
304
    }
305
306
    /**
307
     * @param string     $contentType
308
     * @param Parameters $requestParameters
309
     *
310
     * @return array
311
     */
312 22
    private function getDefaultValues($contentType, Parameters $requestParameters)
313
    {
314 22
        $path = [];
315 22
        $query = [];
316 22
        $headers = ['Content-Type' => $contentType];
317 22
        $body = null;
318 22
        $formData = [];
319
320
        /**
321
         * @var string    $name
322
         * @var Parameter $parameter
323
         */
324 22
        foreach ($requestParameters->getIterator() as $name => $parameter) {
325 10
            if (isset($parameter->getSchema()->default)) {
326 10
                $value = $parameter->getSchema()->default;
327 10
                switch ($parameter->getLocation()) {
328 10
                    case 'path':
329 6
                        $path[$name] = $value;
330 6
                        break;
331 9
                    case 'query':
332 6
                        $query[$name] = $value;
333 6
                        break;
334 8
                    case 'header':
335 6
                        $headers[$name] = $value;
336 6
                        break;
337 2
                    case 'formData':
338 1
                        $formData[$name] = sprintf("%s=%s", $name, $value);
339 1
                        break;
340 1
                    case 'body':
341 1
                        $body = $this->serializeRequestBody($value, $contentType);
342 1
                        break;
343
                }
344
            }
345
        }
346
347 22
        return [$path, $query, $headers, $body, $formData];
348
    }
349
350
351
    /**
352
     * Create a complete API Uri from the Base Uri, path and query parameters.
353
     *
354
     * Example:
355
     *  Given a base uri that equal http://domain.tld
356
     *  Given the following parameters /pets/{id}, ['id' => 1], ['foo' => 'bar']
357
     *  Then the Uri will equal to http://domain.tld/pets/1?foo=bar
358
     *
359
     * @param string $pathTemplate    A template path
360
     * @param array  $pathParameters  Path parameters
361
     * @param array  $queryParameters Query parameters
362
     *
363
     * @return UriInterface
364
     */
365 21
    private function buildRequestUri($pathTemplate, array $pathParameters, array $queryParameters)
366
    {
367 21
        $path = $this->uriTemplate->expand($pathTemplate, $pathParameters);
368 21
        $query = http_build_query($queryParameters);
369
370 21
        return $this->baseUri->withPath($path)->withQuery($query);
371
    }
372
373
    /**
374
     * @param array  $decodedBody
375
     * @param string $contentType
376
     *
377
     * @return string
378
     */
379 3
    private function serializeRequestBody(array $decodedBody, $contentType)
380
    {
381 3
        return $this->serializer->serialize(
382 3
            $decodedBody,
383 3
            DecoderUtils::extractFormatFromContentType($contentType)
384
        );
385
    }
386
387
    /**
388
     * Transform a given response into a denormalized PHP object
389
     * If the config option "returnResponse" is set to TRUE, it return a Response instead
390
     *
391
     * @param ResponseInterface  $response
392
     * @param ResponseDefinition $definition
393
     * @param RequestInterface   $request
394
     *
395
     * @return Resource|mixed
396
     */
397 17
    private function getDataFromResponse(
398
        ResponseInterface $response,
399
        ResponseDefinition $definition,
400
        RequestInterface $request
401
    ) {
402 17
        if ($this->config['returnResponse'] === true) {
403 1
            return $response;
404
        }
405
406
        // @todo Find a better way to handle responses with a body definition
407 16
        if (!$definition->hasBodySchema()) {
408 11
            return new Item([], $request->getHeaders());
409
        }
410
411 5
        return $this->serializer->deserialize(
412 5
            (string) $response->getBody(),
413 5
            Resource::class,
414 5
            DecoderUtils::extractFormatFromContentType($response->getHeaderLine('Content-Type')),
415
            [
416 5
                'response' => $response,
417 5
                'responseDefinition' => $definition,
418 5
                'request' => $request,
419
            ]
420
        );
421
    }
422
423
    /**
424
     * Validate a Request message
425
     * If the config option "withRequestValidation" is set to FALSE it won't validate the Request
426
     *
427
     * @param RequestInterface  $request
428
     * @param RequestDefinition $definition
429
     *
430
     * @throws ConstraintViolations
431
     */
432 20
    private function validateRequest(RequestInterface $request, RequestDefinition $definition)
433
    {
434 20
        if ($this->config['validateRequest'] === false) {
435 2
            return;
436
        }
437
438 18
        $this->messageValidator->validateRequest($request, $definition);
439 18
        if ($this->messageValidator->hasViolations()) {
440 2
            throw new RequestViolations(
441 2
                $this->messageValidator->getViolations()
442
            );
443
        }
444 16
    }
445
446
    /**
447
     * Validate a Response message
448
     * If the config option "withResponseValidation" is set to FALSE it won't validate the Response
449
     *
450
     * @param ResponseInterface $response
451
     * @param RequestDefinition $definition
452
     *
453
     * @throws ConstraintViolations
454
     */
455 18
    private function validateResponse(ResponseInterface $response, RequestDefinition $definition)
456
    {
457 18
        if ($this->config['validateResponse'] === false) {
458 10
            return;
459
        }
460
461 8
        $this->messageValidator->validateResponse($response, $definition);
462 8
        if ($this->messageValidator->hasViolations()) {
463 2
            throw new ResponseViolations(
464 2
                $this->messageValidator->getViolations()
465
            );
466
        }
467 6
    }
468
}
469