Completed
Push — master ( 20b0b4...0ce164 )
by Camilo
13s
created

TgLog::buildMultipartFormData()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3

Importance

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

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
156
                    $deferred->resolve(new TelegramDocument($exception->getResponse()));
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getResponse() does only exist in the following sub-classes of Exception: GuzzleHttp\Exception\BadResponseException, GuzzleHttp\Exception\ClientException, GuzzleHttp\Exception\ConnectException, GuzzleHttp\Exception\RequestException, GuzzleHttp\Exception\ServerException, GuzzleHttp\Exception\TooManyRedirectsException. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
157
                } else {
158
                    $deferred->reject($exception);
159
                }
160
            }
161
        );
162
    }
163
164
    /**
165
     * Builds up the Telegram API url
166
     * @return TgLog
167
     */
168 40
    final private function constructApiUrl(): TgLog
169
    {
170 40
        $this->apiUrl = 'https://api.telegram.org/bot' . $this->botToken . '/';
171 40
        $this->logger->debug('Built up the API URL');
172 40
        return $this;
173
    }
174
175
    /**
176
     * This is the method that actually makes the call, which can be easily overwritten so that our unit tests can work
177
     *
178
     * @param TelegramMethods $method
179
     * @param array $formData
180
     *
181
     * @return TelegramRawData
182
     * @throws \Exception
183
     */
184 24
    protected function sendRequestToTelegram(TelegramMethods $method, array $formData): TelegramRawData
185
    {
186 24
        $e = null;
187 24
        $this->logger->debug('About to perform HTTP call to Telegram\'s API');
188
        try {
189 24
            $response = $this->requestHandler->post($this->composeApiMethodUrl($method), $formData);
190 24
            $this->logger->debug('Got response back from Telegram');
191 24
            return $response;
192
        } catch (\Exception $e) {
193
            // It can happen that we have a network problem, in such case, we can't do nothing about it, so rethrow
194
            if (!method_exists($e, 'getResponse') || empty($e->getResponse())) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getResponse() does only exist in the following sub-classes of Exception: GuzzleHttp\Exception\BadResponseException, GuzzleHttp\Exception\ClientException, GuzzleHttp\Exception\ConnectException, GuzzleHttp\Exception\RequestException, GuzzleHttp\Exception\ServerException, GuzzleHttp\Exception\TooManyRedirectsException. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
195
                throw $e;
196
            }
197
            return new TelegramRawData((string)$e->getResponse()->getBody(), $e);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getResponse() does only exist in the following sub-classes of Exception: GuzzleHttp\Exception\BadResponseException, GuzzleHttp\Exception\ClientException, GuzzleHttp\Exception\ConnectException, GuzzleHttp\Exception\RequestException, GuzzleHttp\Exception\ServerException, GuzzleHttp\Exception\TooManyRedirectsException. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
198
        }
199
    }
200
201
    /**
202
     * @param TelegramMethods $method
203
     * @param array $formData
204
     *
205
     * @return PromiseInterface
206
     */
207
    protected function sendAsyncRequestToTelegram(TelegramMethods $method, array $formData): PromiseInterface
208
    {
209
        $this->logger->debug('About to perform async HTTP call to Telegram\'s API');
210
        $deferred = new Deferred();
211
212
        $promise = $this->requestHandler->postAsync($this->composeApiMethodUrl($method), $formData);
213
        $promise->then(
214
            function (ResponseInterface $response) use ($deferred) {
215
                $deferred->resolve(new TelegramRawData((string)$response->getBody()));
216
            },
217
            function (\Exception $exception) use ($deferred) {
218
                if (method_exists($exception, 'getResponse') && !empty($exception->getResponse()->getBody())) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getResponse() does only exist in the following sub-classes of Exception: GuzzleHttp\Exception\BadResponseException, GuzzleHttp\Exception\ClientException, GuzzleHttp\Exception\ConnectException, GuzzleHttp\Exception\RequestException, GuzzleHttp\Exception\ServerException, GuzzleHttp\Exception\TooManyRedirectsException. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
219
                    $deferred->resolve(new TelegramRawData((string)$exception->getResponse()->getBody(), $exception));
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getResponse() does only exist in the following sub-classes of Exception: GuzzleHttp\Exception\BadResponseException, GuzzleHttp\Exception\ClientException, GuzzleHttp\Exception\ConnectException, GuzzleHttp\Exception\RequestException, GuzzleHttp\Exception\ServerException, GuzzleHttp\Exception\TooManyRedirectsException. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
220
                } else {
221
                    $deferred->reject($exception);
222
                }
223
            }
224
        );
225
226
        return $deferred->promise();
227
    }
228
229
    /**
230
     * Resets everything to the default values
231
     *
232
     * @return TgLog
233
     */
234 31
    private function resetObjectValues(): TgLog
235
    {
236 31
        $this->formConstructor->formType = 'application/x-www-form-urlencoded';
237 31
        return $this;
238
    }
239
240
    /**
241
     * Builds up the URL with which we can work with
242
     *
243
     * All methods in the Bot API are case-insensitive.
244
     * All queries must be made using UTF-8.
245
     *
246
     * @see https://core.telegram.org/bots/api#making-requests
247
     *
248
     * @param TelegramMethods $call
249
     * @return string
250
     */
251 30
    protected function composeApiMethodUrl(TelegramMethods $call): string
252
    {
253 30
        $completeClassName = get_class($call);
254 30
        $this->methodName = substr($completeClassName, strrpos($completeClassName, '\\') + 1);
255 30
        $this->logger->info('About to perform API request', ['method' => $this->methodName]);
256
257 30
        return $this->apiUrl . $this->methodName;
258
    }
259
260
    /**
261
     * @param TelegramRawData $telegramRawData
262
     * @return TgLog
263
     * @throws CustomClientException
264
     */
265
    private function handleOffErrorRequest(TelegramRawData $telegramRawData): TgLog
266
    {
267
        $errorRequest = new UnsuccessfulRequest($telegramRawData->getErrorData(), $this->logger);
268
269
        $clientException = new CustomClientException(
270
            $errorRequest->description,
271
            $errorRequest->error_code,
272
            $telegramRawData->getException()
273
        );
274
        $clientException->setParameters($errorRequest->parameters);
275
        throw $clientException;
276
    }
277
}
278