Completed
Pull Request — master (#50)
by Rick
02:03
created

TgLog   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 359
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 56.2%

Importance

Changes 0
Metric Value
wmc 28
lcom 1
cbo 13
dl 0
loc 359
ccs 68
cts 121
cp 0.562
rs 10
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 3
A performApiRequest() 0 11 2
A performAsyncApiRequest() 0 6 1
A downloadFile() 0 7 1
A downloadFileAsync() 0 20 2
A constructApiUrl() 0 6 1
A sendRequestToTelegram() 0 18 3
A sendAsyncRequestToTelegram() 0 20 2
A resetObjectValues() 0 7 1
B constructFormData() 0 29 3
A checkSpecialConditions() 0 21 4
A composeApiMethodUrl() 0 8 1
B buildMultipartFormData() 0 25 3
A handleOffErrorRequest() 0 12 1
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace unreal4u\TelegramAPI;
6
7
use GuzzleHttp\Client;
8
use GuzzleHttp\ClientInterface;
9
use GuzzleHttp\Exception\ClientException;
10
use GuzzleHttp\Exception\RequestException;
11
use GuzzleHttp\Promise\Promise;
12
use GuzzleHttp\Promise\PromiseInterface;
13
use Psr\Http\Message\ResponseInterface;
14
use Psr\Log\LoggerInterface;
15
use unreal4u\TelegramAPI\Abstracts\TelegramMethods;
16
use unreal4u\TelegramAPI\Abstracts\TelegramTypes;
17
use unreal4u\TelegramAPI\Exceptions\ClientException as CustomClientException;
18
use unreal4u\TelegramAPI\InternalFunctionality\DummyLogger;
19
use unreal4u\TelegramAPI\InternalFunctionality\TelegramDocument;
20
use unreal4u\TelegramAPI\InternalFunctionality\TelegramRawData;
21
use unreal4u\TelegramAPI\Telegram\Types\Custom\InputFile;
22
use unreal4u\TelegramAPI\Telegram\Types\Custom\UnsuccessfulRequest;
23
use unreal4u\TelegramAPI\Telegram\Types\File;
24
25
/**
26
 * The main API which does it all
27
 */
28
class TgLog
29
{
30
    /**
31
     * @var ClientInterface
32
     */
33
    protected $httpClient;
34
35
    /**
36
     * Stores the token
37
     * @var string
38
     */
39
    private $botToken;
40
41
    /**
42
     * Contains an instance to a PSR-3 compatible logger
43
     * @var LoggerInterface
44
     */
45
    protected $logger;
46
47
    /**
48
     * Stores the API URL from Telegram
49
     * @var string
50
     */
51
    private $apiUrl = '';
52
53
    /**
54
     * With this flag we'll know what type of request to send to Telegram
55
     *
56
     * 'application/x-www-form-urlencoded' is the "normal" one, which is simpler and quicker.
57
     * 'multipart/form-data' should be used only when you upload documents, photos, etc.
58
     *
59
     * @var string
60
     */
61
    private $formType = 'application/x-www-form-urlencoded';
62
63
    /**
64
     * Stores the last method name used
65
     * @var string
66
     */
67
    protected $methodName = '';
68
69
    /**
70
     * TelegramLog constructor.
71
     * @param string $botToken
72
     * @param LoggerInterface $logger
73
     * @param Client $client Optional Guzzle object
74
     */
75 39
    public function __construct(string $botToken, LoggerInterface $logger = null, Client $client = null)
76
    {
77 39
        $this->botToken = $botToken;
78
79
        // Initialize new dummy logger (PSR-3 compatible) if not injected
80 39
        if ($logger === null) {
81 39
            $logger = new DummyLogger();
82
        }
83 39
        $this->logger = $logger;
84
85
        // Initialize new Guzzle client if not injected
86 39
        if ($client === null) {
87 39
            $client = new Client();
88
        }
89 39
        $this->httpClient = $client;
90
91 39
        $this->constructApiUrl();
92 39
    }
93
94
    /**
95
     * Prepares and sends an API request to Telegram
96
     *
97
     * @param TelegramMethods $method
98
     * @return TelegramTypes
99
     * @throws \unreal4u\TelegramAPI\Exceptions\MissingMandatoryField
100
     */
101 30
    public function performApiRequest(TelegramMethods $method): TelegramTypes
102
    {
103 30
        $this->logger->debug('Request for API call, resetting internal values', [get_class($method)]);
104 30
        $this->resetObjectValues();
105 30
        $telegramRawData = $this->sendRequestToTelegram($method, $this->constructFormData($method));
106 23
        if ($telegramRawData->isError()) {
107
            $this->handleOffErrorRequest($telegramRawData);
108
        }
109
110 23
        return $method::bindToObject($telegramRawData, $this->logger);
111
    }
112
113
    /**
114
     * @param TelegramMethods $method
115
     *
116
     * @return PromiseInterface
117
     */
118
    public function performAsyncApiRequest(TelegramMethods $method)
119
    {
120
        $this->logger->debug('Request for async API call, resetting internal values', [get_class($method)]);
121
        $this->resetObjectValues();
122
        return $this->sendAsyncRequestToTelegram($method, $this->constructFormData($method));
123
    }
124
125
    /**
126
     * Will download a file from the Telegram server. Before calling this function, you have to call the getFile method!
127
     *
128
     * @see \unreal4u\TelegramAPI\Telegram\Types\File
129
     * @see \unreal4u\TelegramAPI\Telegram\Methods\GetFile
130
     *
131
     * @param File $file
132
     * @return TelegramDocument
133
     */
134
    public function downloadFile(File $file): TelegramDocument
135
    {
136
        $this->logger->debug('Downloading file from Telegram, creating URL');
137
        $url = 'https://api.telegram.org/file/bot' . $this->botToken . '/' . $file->file_path;
138
        $this->logger->debug('About to perform request to begin downloading file');
139
        return new TelegramDocument($this->httpClient->get($url));
140
    }
141
142
    /**
143
     * @param File $file
144
     *
145
     * @return PromiseInterface
146
     */
147
    public function downloadFileAsync(File $file): PromiseInterface
148
    {
149
        $this->logger->debug('Downloading file async from Telegram, creating URL');
150
        $url = 'https://api.telegram.org/file/bot' . $this->botToken . '/' . $file->file_path;
151
        $this->logger->debug('About to perform request to begin downloading file');
152
        
153
        $deferred = new Promise();
154
        
155
        return $this->httpClient->getAsync($url)->then(function (ResponseInterface $response) use ($deferred)
156
        {
157
            $deferred->resolve(new TelegramDocument($response));
158
        },
159
        function (RequestException $exception) use ($deferred)
160
        {
161
            if (!empty($exception->getResponse()->getBody()))
162
                $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...
163
            else
164
                $deferred->reject($exception);
165
        });
166
    }
167
168
    /**
169
     * Builds up the Telegram API url
170
     * @return TgLog
171
     */
172 39
    final private function constructApiUrl(): TgLog
173
    {
174 39
        $this->apiUrl = 'https://api.telegram.org/bot' . $this->botToken . '/';
175 39
        $this->logger->debug('Built up the API URL');
176 39
        return $this;
177
    }
178
179
    /**
180
     * This is the method that actually makes the call, which can be easily overwritten so that our unit tests can work
181
     *
182
     * @param TelegramMethods $method
183
     * @param array $formData
184
     * @return TelegramRawData
185
     */
186 23
    protected function sendRequestToTelegram(TelegramMethods $method, array $formData): TelegramRawData
187
    {
188 23
        $e = null;
189 23
        $this->logger->debug('About to perform HTTP call to Telegram\'s API');
190
        try {
191
            /** @noinspection PhpMethodParametersCountMismatchInspection */
192 23
            $response = $this->httpClient->post($this->composeApiMethodUrl($method), $formData);
193 23
            $this->logger->debug('Got response back from Telegram, applying json_decode');
194
        } catch (ClientException $e) {
195
            $response = $e->getResponse();
196
            // It can happen that we have a network problem, in such case, we can't do nothing about it, so rethrow
197
            if (empty($response)) {
198
                throw $e;
199
            }
200
        } finally {
201 23
            return new TelegramRawData((string)$response->getBody(), $e);
202
        }
203
    }
204
205
    /**
206
     * @param TelegramMethods $method
207
     * @param array $formData
208
     *
209
     * @return PromiseInterface
210
     */
211
    protected function sendAsyncRequestToTelegram(TelegramMethods $method, array $formData): PromiseInterface
212
    {
213
        $this->logger->debug('About to perform async HTTP call to Telegram\'s API');
214
        $deferred = new Promise();
215
        
216
        $promise = $this->httpClient->postAsync($this->composeApiMethodUrl($method), $formData);
217
        $promise->then(function (ResponseInterface $response) use ($deferred)
218
        {
219
            $deferred->resolve(new TelegramRawData((string) $response->getBody()));
220
        },
221
        function (RequestException $exception) use ($deferred)
222
        {
223
            if (!empty($exception->getResponse()->getBody()))
224
                $deferred->resolve(new TelegramRawData((string) $exception->getResponse()->getBody(), $exception));
225
            else
226
                $deferred->reject($exception);
227
        });
228
        
229
        return $deferred;
230
    }
231
232
    /**
233
     * Resets everything to the default values
234
     *
235
     * @return TgLog
236
     */
237 30
    private function resetObjectValues(): TgLog
238
    {
239 30
        $this->formType = 'application/x-www-form-urlencoded';
240 30
        $this->methodName = '';
241
242 30
        return $this;
243
    }
244
245
    /**
246
     * Builds up the form elements to be sent to Telegram
247
     *
248
     * @TODO Move this to apart function
249
     *
250
     * @param TelegramMethods $method
251
     * @return array
252
     * @throws \unreal4u\TelegramAPI\Exceptions\MissingMandatoryField
253
     */
254 30
    private function constructFormData(TelegramMethods $method): array
255
    {
256 30
        $result = $this->checkSpecialConditions($method);
257
258 30
        switch ($this->formType) {
259 30
            case 'application/x-www-form-urlencoded':
260 26
                $this->logger->debug('Creating x-www-form-urlencoded form (AKA fast request)');
261
                $formData = [
262 26
                    'form_params' => $method->export(),
263
                ];
264 24
                break;
265 4
            case 'multipart/form-data':
266 4
                $formData = $this->buildMultipartFormData($method->export(), $result['id'], $result['stream']);
267 4
                break;
268
            default:
269
                $this->logger->critical(sprintf(
270
                    'Invalid form-type detected, if you incur in such a situation, this is most likely a product to '.
271
                    'a bug. Please copy entire line and report at %s',
272
                    'https://github.com/unreal4u/telegram-api/issues'
273
                ), [
274
                    'formType' => $this->formType
275
                ]);
276
                $formData = [];
277
                break;
278
        }
279 28
        $this->logger->debug('About to send following data', $formData);
280
281 28
        return $formData;
282
    }
283
284
    /**
285
     * Can perform any special checks needed to be performed before sending the actual request to Telegram
286
     *
287
     * This will return an array with data that will be different in each case (for now). This can be changed in the
288
     * future.
289
     *
290
     * @param TelegramMethods $method
291
     * @return array
292
     */
293 30
    private function checkSpecialConditions(TelegramMethods $method): array
294
    {
295 30
        $this->logger->debug('Checking whether to apply special conditions to this request');
296 30
        $method->performSpecialConditions();
297
298 30
        $return = [false];
299
300 30
        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...
301 26
            if (is_object($value) && $value instanceof InputFile) {
302 4
                $this->logger->debug('About to send a file, so changing request to use multi-part instead');
303
                // If we are about to send a file, we must use the multipart/form-data way
304 4
                $this->formType = 'multipart/form-data';
305
                $return = [
306 4
                    'id' => $key,
307 26
                    'stream' => $value->getStream(),
308
                ];
309
            }
310
        }
311
312 30
        return $return;
313
    }
314
315
    /**
316
     * Builds up the URL with which we can work with
317
     *
318
     * All methods in the Bot API are case-insensitive.
319
     * All queries must be made using UTF-8.
320
     *
321
     * @see https://core.telegram.org/bots/api#making-requests
322
     *
323
     * @param TelegramMethods $call
324
     * @return string
325
     */
326 29
    protected function composeApiMethodUrl(TelegramMethods $call): string
327
    {
328 29
        $completeClassName = get_class($call);
329 29
        $this->methodName = substr($completeClassName, strrpos($completeClassName, '\\') + 1);
330 29
        $this->logger->info('About to perform API request', ['method' => $this->methodName]);
331
332 29
        return $this->apiUrl . $this->methodName;
333
    }
334
335
    /**
336
     * Builds up a multipart form-like array for Guzzle
337
     *
338
     * @param array $data The original object in array form
339
     * @param string $fileKeyName A file handler will be sent instead of a string, state here which field it is
340
     * @param resource $stream The actual file handler
341
     * @return array Returns the actual formdata to be sent
342
     */
343 4
    private function buildMultipartFormData(array $data, string $fileKeyName, $stream): array
344
    {
345 4
        $this->logger->debug('Creating multi-part form array data (complex and expensive)');
346
        $formData = [
347 4
            'multipart' => [],
348
        ];
349
350 4
        foreach ($data as $id => $value) {
351
            // Always send as a string unless it's a file
352
            $multiPart = [
353 4
                'name' => $id,
354
                'contents' => null,
355
            ];
356
357 4
            if ($id === $fileKeyName) {
358 4
                $multiPart['contents'] = $stream;
359
            } else {
360 4
                $multiPart['contents'] = (string)$value;
361
            }
362
363 4
            $formData['multipart'][] = $multiPart;
364
        }
365
366 4
        return $formData;
367
    }
368
369
    /**
370
     * @param TelegramRawData $telegramRawData
371
     * @return TgLog
372
     * @throws CustomClientException
373
     */
374
    private function handleOffErrorRequest(TelegramRawData $telegramRawData): TgLog
375
    {
376
        $errorRequest = new UnsuccessfulRequest($telegramRawData->getErrorData(), $this->logger);
377
378
        $clientException = new CustomClientException(
379
            $errorRequest->description,
380
            $errorRequest->error_code,
381
            $telegramRawData->getException()
382
        );
383
        $clientException->setParameters($errorRequest->parameters);
384
        throw $clientException;
385
    }
386
}
387