Completed
Pull Request — master (#48)
by Rick
03:30
created

TgLog::handleOffErrorRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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