Completed
Push — master ( 0a69d4...44fef3 )
by Camilo
03:25 queued 01:10
created

TgLog::performApiRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.3149

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 4
cts 7
cp 0.5714
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
crap 2.3149
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace unreal4u\TelegramAPI;
6
7
use GuzzleHttp\Exception\ClientException;
8
use GuzzleHttp\Exception\RequestException;
9
use GuzzleHttp\Promise\Promise;
10
use GuzzleHttp\Promise\PromiseInterface;
11
use Psr\Http\Message\ResponseInterface;
12
use Psr\Log\LoggerInterface;
13
use unreal4u\TelegramAPI\Abstracts\TelegramMethods;
14
use unreal4u\TelegramAPI\Abstracts\TelegramTypes;
15
use unreal4u\TelegramAPI\Exceptions\ClientException as CustomClientException;
16
use unreal4u\TelegramAPI\InternalFunctionality\DummyLogger;
17
use unreal4u\TelegramAPI\InternalFunctionality\TelegramDocument;
18
use unreal4u\TelegramAPI\InternalFunctionality\TelegramRawData;
19
use unreal4u\TelegramAPI\Telegram\Types\Custom\InputFile;
20
use unreal4u\TelegramAPI\Telegram\Types\Custom\UnsuccessfulRequest;
21
use unreal4u\TelegramAPI\Telegram\Types\File;
22
23
/**
24
 * The main API which does it all
25
 */
26
class TgLog
27
{
28
    /**
29
     * @var RequestHandlerInterface
30
     */
31
    protected $requestHandler;
32
33
    /**
34
     * Stores the token
35
     * @var string
36
     */
37
    private $botToken;
38
39
    /**
40
     * Contains an instance to a PSR-3 compatible logger
41
     * @var LoggerInterface
42
     */
43
    protected $logger;
44
45
    /**
46
     * Stores the API URL from Telegram
47
     * @var string
48
     */
49
    private $apiUrl = '';
50
51
    /**
52
     * With this flag we'll know what type of request to send to Telegram
53
     *
54
     * 'application/x-www-form-urlencoded' is the "normal" one, which is simpler and quicker.
55
     * 'multipart/form-data' should be used only when you upload documents, photos, etc.
56
     *
57
     * @var string
58
     */
59
    private $formType = 'application/x-www-form-urlencoded';
60
61
    /**
62
     * Stores the last method name used
63
     * @var string
64
     */
65
    protected $methodName = '';
66
67
    /**
68
     * TelegramLog constructor.
69
     *
70
     * @param string $botToken
71
     * @param LoggerInterface $logger
72
     * @param RequestHandlerInterface $handler
73
     */
74 16
    public function __construct(string $botToken, LoggerInterface $logger = null, RequestHandlerInterface $handler = null)
75
    {
76 16
        $this->botToken = $botToken;
77
78
        // Initialize new dummy logger (PSR-3 compatible) if not injected
79 16
        if ($logger === null) {
80 16
            $logger = new DummyLogger();
81
        }
82 16
        $this->logger = $logger;
83
84
        // Initialize new Guzzle client if not injected
85 16
        if ($handler === null) {
86 16
            $handler = new GuzzleRequestHandler(null, $logger);
87
        }
88 16
        $this->requestHandler = $handler;
89
90 16
        $this->constructApiUrl();
91 16
    }
92
93
    /**
94
     * Prepares and sends an API request to Telegram
95
     *
96
     * @param TelegramMethods $method
97
     * @return TelegramTypes
98
     * @throws \unreal4u\TelegramAPI\Exceptions\MissingMandatoryField
99
     */
100 7
    public function performApiRequest(TelegramMethods $method): TelegramTypes
101
    {
102 7
        $this->logger->debug('Request for API call, resetting internal values', [get_class($method)]);
103 7
        $this->resetObjectValues();
104 7
        $telegramRawData = $this->sendRequestToTelegram($method, $this->constructFormData($method));
105
        if ($telegramRawData->isError()) {
106
            $this->handleOffErrorRequest($telegramRawData);
107
        }
108
109
        return $method::bindToObject($telegramRawData, $this->logger);
110
    }
111
112
    /**
113
     * @param TelegramMethods $method
114
     *
115
     * @return PromiseInterface
116
     */
117
    public function performAsyncApiRequest(TelegramMethods $method)
118
    {
119
        $this->logger->debug('Request for async API call, resetting internal values', [get_class($method)]);
120
        $this->resetObjectValues();
121
        return $this->sendAsyncRequestToTelegram($method, $this->constructFormData($method));
122
    }
123
124
    /**
125
     * Will download a file from the Telegram server. Before calling this function, you have to call the getFile method!
126
     *
127
     * @see \unreal4u\TelegramAPI\Telegram\Types\File
128
     * @see \unreal4u\TelegramAPI\Telegram\Methods\GetFile
129
     *
130
     * @param File $file
131
     * @return TelegramDocument
132
     */
133
    public function downloadFile(File $file): TelegramDocument
134
    {
135
        $this->logger->debug('Downloading file from Telegram, creating URL');
136
        $url = 'https://api.telegram.org/file/bot' . $this->botToken . '/' . $file->file_path;
137
        $this->logger->debug('About to perform request to begin downloading file');
138
        return new TelegramDocument($this->requestHandler->get($url));
139
    }
140
141
    /**
142
     * @param File $file
143
     *
144
     * @return PromiseInterface
145
     */
146
    public function downloadFileAsync(File $file): PromiseInterface
147
    {
148
        $this->logger->debug('Downloading file async from Telegram, creating URL');
149
        $url = 'https://api.telegram.org/file/bot' . $this->botToken . '/' . $file->file_path;
150
        $this->logger->debug('About to perform request to begin downloading file');
151
152
        $deferred = new Promise();
153
154
        return $this->requestHandler->requestAsync($url)->then(function (ResponseInterface $response) use ($deferred)
0 ignored issues
show
Bug introduced by
The call to requestAsync() misses a required argument $formData.

This check looks for function calls that miss required arguments.

Loading history...
155
        {
156
            $deferred->resolve(new TelegramDocument($response));
157
        },
158
            function (RequestException $exception) use ($deferred)
159
            {
160
                if (!empty($exception->getResponse()->getBody()))
161
                    $deferred->resolve(new TelegramDocument($exception->getResponse()));
0 ignored issues
show
Bug introduced by
It seems like $exception->getResponse() can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
162
                else
163
                    $deferred->reject($exception);
164
            });
165
    }
166
167
    /**
168
     * Builds up the Telegram API url
169
     * @return TgLog
170
     */
171 16
    final private function constructApiUrl(): TgLog
172
    {
173 16
        $this->apiUrl = 'https://api.telegram.org/bot' . $this->botToken . '/';
174 16
        $this->logger->debug('Built up the API URL');
175 16
        return $this;
176
    }
177
178
    /**
179
     * This is the method that actually makes the call, which can be easily overwritten so that our unit tests can work
180
     *
181
     * @param TelegramMethods $method
182
     * @param array $formData
183
     * @return TelegramRawData
184
     */
185
    protected function sendRequestToTelegram(TelegramMethods $method, array $formData): TelegramRawData
186
    {
187
        $e = null;
188
        $this->logger->debug('About to perform HTTP call to Telegram\'s API');
189
        try {
190
            /** @noinspection PhpMethodParametersCountMismatchInspection */
191
            $response = $this->requestHandler->request($this->composeApiMethodUrl($method), $formData);
192
            $this->logger->debug('Got response back from Telegram, applying json_decode');
193
        } catch (ClientException $e) {
194
            $response = $e->getResponse();
195
            // It can happen that we have a network problem, in such case, we can't do nothing about it, so rethrow
196
            if (empty($response)) {
197
                throw $e;
198
            }
199
        } finally {
200
            return new TelegramRawData((string)$response->getBody(), $e);
0 ignored issues
show
Bug introduced by
The method getBody does only exist in Psr\Http\Message\ResponseInterface, but not in unreal4u\TelegramAPI\Int...onality\TelegramRawData.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
201
        }
202
    }
203
204
    /**
205
     * @param TelegramMethods $method
206
     * @param array $formData
207
     *
208
     * @return PromiseInterface
209
     */
210
    protected function sendAsyncRequestToTelegram(TelegramMethods $method, array $formData): PromiseInterface
211
    {
212
        $this->logger->debug('About to perform async HTTP call to Telegram\'s API');
213
        $deferred = new Promise();
214
215
        $promise = $this->requestHandler->requestAsync($this->composeApiMethodUrl($method), $formData);
216
        $promise->then(function (ResponseInterface $response) use ($deferred)
217
        {
218
            $deferred->resolve(new TelegramRawData((string) $response->getBody()));
219
        },
220
            function (RequestException $exception) use ($deferred)
221
            {
222
                if (!empty($exception->getResponse()->getBody()))
223
                    $deferred->resolve(new TelegramRawData((string) $exception->getResponse()->getBody(), $exception));
224
                else
225
                    $deferred->reject($exception);
226
            });
227
228
        return $deferred;
229
    }
230
231
    /**
232
     * Resets everything to the default values
233
     *
234
     * @return TgLog
235
     */
236 7
    private function resetObjectValues(): TgLog
237
    {
238 7
        $this->formType = 'application/x-www-form-urlencoded';
239 7
        $this->methodName = '';
240
241 7
        return $this;
242
    }
243
244
    /**
245
     * Builds up the form elements to be sent to Telegram
246
     *
247
     * @TODO Move this to apart function
248
     *
249
     * @param TelegramMethods $method
250
     * @return array
251
     * @throws \unreal4u\TelegramAPI\Exceptions\MissingMandatoryField
252
     */
253 7
    private function constructFormData(TelegramMethods $method): array
254
    {
255 7
        $result = $this->checkSpecialConditions($method);
256
257 7
        switch ($this->formType) {
258 7
            case 'application/x-www-form-urlencoded':
259 7
                $this->logger->debug('Creating x-www-form-urlencoded form (AKA fast request)');
260
                $formData = [
261 7
                    'form_params' => $method->export(),
262
                ];
263 5
                break;
264
            case 'multipart/form-data':
265
                $formData = $this->buildMultipartFormData($method->export(), $result['id'], $result['stream']);
266
                break;
267
            default:
268
                $this->logger->critical(sprintf(
269
                    'Invalid form-type detected, if you incur in such a situation, this is most likely a product to '.
270
                    'a bug. Please copy entire line and report at %s',
271
                    'https://github.com/unreal4u/telegram-api/issues'
272
                ), [
273
                    'formType' => $this->formType
274
                ]);
275
                $formData = [];
276
                break;
277
        }
278 5
        $this->logger->debug('About to send following data', $formData);
279
280 5
        return $formData;
281
    }
282
283
    /**
284
     * Can perform any special checks needed to be performed before sending the actual request to Telegram
285
     *
286
     * This will return an array with data that will be different in each case (for now). This can be changed in the
287
     * future.
288
     *
289
     * @param TelegramMethods $method
290
     * @return array
291
     */
292 7
    private function checkSpecialConditions(TelegramMethods $method): array
293
    {
294 7
        $this->logger->debug('Checking whether to apply special conditions to this request');
295 7
        $method->performSpecialConditions();
296
297 7
        $return = [false];
298
299 7
        foreach ($method as $key => $value) {
0 ignored issues
show
Bug introduced by
The expression $method of type object<unreal4u\Telegram...tracts\TelegramMethods> is not traversable.
Loading history...
300 6
            if (is_object($value) && $value instanceof InputFile) {
301
                $this->logger->debug('About to send a file, so changing request to use multi-part instead');
302
                // If we are about to send a file, we must use the multipart/form-data way
303
                $this->formType = 'multipart/form-data';
304
                $return = [
305
                    'id' => $key,
306 6
                    'stream' => $value->getStream(),
307
                ];
308
            }
309
        }
310
311 7
        return $return;
312
    }
313
314
    /**
315
     * Builds up the URL with which we can work with
316
     *
317
     * All methods in the Bot API are case-insensitive.
318
     * All queries must be made using UTF-8.
319
     *
320
     * @see https://core.telegram.org/bots/api#making-requests
321
     *
322
     * @param TelegramMethods $call
323
     * @return string
324
     */
325 6
    protected function composeApiMethodUrl(TelegramMethods $call): string
326
    {
327 6
        $completeClassName = get_class($call);
328 6
        $this->methodName = substr($completeClassName, strrpos($completeClassName, '\\') + 1);
329 6
        $this->logger->info('About to perform API request', ['method' => $this->methodName]);
330
331 6
        return $this->apiUrl . $this->methodName;
332
    }
333
334
    /**
335
     * Builds up a multipart form-like array for Guzzle
336
     *
337
     * @param array $data The original object in array form
338
     * @param string $fileKeyName A file handler will be sent instead of a string, state here which field it is
339
     * @param resource $stream The actual file handler
340
     * @return array Returns the actual formdata to be sent
341
     */
342
    private function buildMultipartFormData(array $data, string $fileKeyName, $stream): array
343
    {
344
        $this->logger->debug('Creating multi-part form array data (complex and expensive)');
345
        $formData = [
346
            'multipart' => [],
347
        ];
348
349
        foreach ($data as $id => $value) {
350
            // Always send as a string unless it's a file
351
            $multiPart = [
352
                'name' => $id,
353
                'contents' => null,
354
            ];
355
356
            if ($id === $fileKeyName) {
357
                $multiPart['contents'] = $stream;
358
            } else {
359
                $multiPart['contents'] = (string)$value;
360
            }
361
362
            $formData['multipart'][] = $multiPart;
363
        }
364
365
        return $formData;
366
    }
367
368
    /**
369
     * @param TelegramRawData $telegramRawData
370
     * @return TgLog
371
     * @throws CustomClientException
372
     */
373
    private function handleOffErrorRequest(TelegramRawData $telegramRawData): TgLog
374
    {
375
        $errorRequest = new UnsuccessfulRequest($telegramRawData->getErrorData(), $this->logger);
376
377
        $clientException = new CustomClientException(
378
            $errorRequest->description,
379
            $errorRequest->error_code,
380
            $telegramRawData->getException()
381
        );
382
        $clientException->setParameters($errorRequest->parameters);
383
        throw $clientException;
384
    }
385
}