Issues (25)

src/ApiService.php (2 issues)

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