Completed
Push — master ( dd6f58...272b48 )
by Camilo
05:12
created

TgLog::buildMultipartFormData()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

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