Passed
Push — master ( 302660...3652f8 )
by payever
04:19 queued 01:32
created

CurlClient   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 296
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 38
eloc 140
c 4
b 0
f 0
dl 0
loc 296
rs 9.36

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
A setLogger() 0 3 1
F executeRequest() 0 107 22
A execute() 0 21 3
A download() 0 21 3
A getRequestOptions() 0 13 1
A downloadRequest() 0 39 4
A setLogLevel() 0 3 1
A setLogLevelOnce() 0 3 1
1
<?php
2
3
/**
4
 * PHP version 5.4 and 8
5
 *
6
 * @category  Http
7
 * @package   Payever\Core
8
 * @author    payever GmbH <[email protected]>
9
 * @copyright 2017-2021 payever GmbH
10
 * @license   MIT <https://opensource.org/licenses/MIT>
11
 * @link      https://docs.payever.org/shopsystems/api/getting-started
12
 */
13
14
namespace Payever\ExternalIntegration\Core\Http\Client;
15
16
use Payever\ExternalIntegration\Core\Base\HttpClientInterface;
17
use Payever\ExternalIntegration\Core\Base\RequestInterface;
18
use Payever\ExternalIntegration\Core\Exception\PayeverCommunicationException;
19
use Payever\ExternalIntegration\Core\Http\Request;
20
use Payever\ExternalIntegration\Core\Http\Response;
21
use Psr\Log\LoggerAwareInterface;
22
use Psr\Log\LoggerInterface;
23
use Psr\Log\LogLevel;
24
25
/**
26
 * This class represents Curl implementation of Client
27
 * @SuppressWarnings(PHPMD.MissingImport)
28
 */
29
class CurlClient implements HttpClientInterface, LoggerAwareInterface
30
{
31
    /** @var LoggerInterface */
32
    protected $logger;
33
34
    /** @var string */
35
    protected $logLevel = LogLevel::CRITICAL;
36
37
    /** @var string|null */
38
    protected $tmpLogLevel;
39
40
    /**
41
     * @throws \RuntimeException when cURL extension is not enabled
42
     * @codeCoverageIgnore
43
     */
44
    public function __construct()
45
    {
46
        if (!extension_loaded('curl')) {
47
            throw new \RuntimeException('cURL PHP extension must be enabled in order to use this HTTP client');
48
        }
49
    }
50
51
    /**
52
     * @inheritDoc
53
     */
54
    public function setLogger(LoggerInterface $logger)
55
    {
56
        $this->logger = $logger;
57
    }
58
59
    /**
60
     * @param string $logLevel
61
     */
62
    public function setLogLevel($logLevel = LogLevel::CRITICAL)
63
    {
64
        $this->logLevel = $logLevel;
65
    }
66
67
    /**
68
     * @param string|null $logLevel
69
     */
70
    public function setLogLevelOnce($logLevel)
71
    {
72
        $this->tmpLogLevel = $logLevel;
73
    }
74
75
    /**
76
     * @param RequestInterface $request
77
     * @return Response
78
     * @throws \Exception
79
     */
80
    public function execute(RequestInterface $request)
81
    {
82
        try {
83
            return $this->executeRequest($request);
84
        } catch (\Exception $exception) {
85
            $logLevel = $this->logLevel;
86
            if (null !== $this->tmpLogLevel) {
87
                $logLevel = $this->tmpLogLevel;
88
                $this->tmpLogLevel = null;
89
            }
90
            $this->logger->log(
91
                $logLevel,
92
                sprintf(
93
                    'HTTP Request failed: %s %s',
94
                    $exception->getCode(),
95
                    $exception->getMessage()
96
                ),
97
                ['trace' => $exception->getTraceAsString()]
98
            );
99
100
            throw $exception;
101
        }
102
    }
103
104
    /**
105
     * @param RequestInterface $request
106
     *
107
     * @return Response
108
     *
109
     * @throws \RuntimeException
110
     * @throws PayeverCommunicationException
111
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
112
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
113
     * @SuppressWarnings(PHPMD.NPathComplexity)
114
     * @SuppressWarnings(PHPMD.ShortVariable)
115
     */
116
    protected function executeRequest(RequestInterface $request)
117
    {
118
        $this->logger->debug(
119
            sprintf('HTTP Request %s %s', $request->getMethod(), $request->getUrl()),
120
            ['headers' => $request->getHeaders(), 'body' => $request->toArray()]
121
        );
122
123
        if (!$request->validate()) {
124
            throw new \RuntimeException('Request entity is not valid');
125
        }
126
127
        $ch = curl_init();
128
129
        if ($ch === false) {
130
            throw new \RuntimeException('Could not get cURL resource');
131
        }
132
133
        $options = [
134
            CURLOPT_URL          => $request->getUrl(),
135
            CURLOPT_HTTPHEADER   => $request->getHeaders(),
136
            CURLOPT_HTTP_VERSION => $request->getProtocolVersion(),
137
        ];
138
139
        $customMethods = [
140
            Request::METHOD_PUT,
141
            Request::METHOD_PATCH,
142
            Request::METHOD_DELETE,
143
        ];
144
145
        if ($request->getMethod() === Request::METHOD_POST) {
146
            $options[CURLOPT_POST] = true;
147
            $options[CURLOPT_POSTFIELDS] = $request->toArray();
148
        } elseif ($request->getMethod() === Request::METHOD_GET && $request->toArray()) {
149
            $paramChar = strpos('?', $request->getUrl()) === false ? '?' : '&';
150
            $options[CURLOPT_URL] = $request->getUrl() . $paramChar . http_build_query($request->toArray());
151
        } elseif (in_array($request->getMethod(), $customMethods)) {
152
            $options[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
153
            $options[CURLOPT_POSTFIELDS] = $request->toArray();
154
        }
155
156
        if (
157
            isset($options[CURLOPT_POSTFIELDS]) && is_array($options[CURLOPT_POSTFIELDS])
158
            && $request->getHeader('Content-Type') == 'application/x-www-form-urlencoded'
159
        ) {
160
            $options[CURLOPT_POSTFIELDS] = http_build_query($options[CURLOPT_POSTFIELDS]);
0 ignored issues
show
Bug introduced by
It seems like $options[Payever\Externa...ent\CURLOPT_POSTFIELDS] can also be of type string; however, parameter $data of http_build_query() does only seem to accept array|object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

160
            $options[CURLOPT_POSTFIELDS] = http_build_query(/** @scrutinizer ignore-type */ $options[CURLOPT_POSTFIELDS]);
Loading history...
161
        } elseif (
162
            isset($options[CURLOPT_POSTFIELDS]) && is_array($options[CURLOPT_POSTFIELDS])
163
            && $request->getHeader('Content-Type') == 'application/json'
164
        ) {
165
            $options[CURLOPT_POSTFIELDS] = json_encode($options[CURLOPT_POSTFIELDS]);
166
        }
167
168
        curl_setopt_array($ch, $this->getRequestOptions($options));
169
170
        $result       = curl_exec($ch);
171
        $httpCode     = curl_getinfo($ch, CURLINFO_HTTP_CODE);
172
        $errorMessage = curl_error($ch);
173
        $errorNumber  = curl_errno($ch);
174
175
        curl_close($ch);
176
177
        $this->logger->debug(
178
            sprintf('HTTP Response %s %s', $request->getMethod(), $request->getUrl()),
179
            ['httpCode' => $httpCode, 'body' => $result, 'curlError' => $errorMessage]
180
        );
181
182
        if ($errorNumber !== 0) {
183
            throw new PayeverCommunicationException($errorMessage, $errorNumber);
184
        }
185
186
        if ($httpCode >= 400) {
187
            $message = $result;
188
            $data = json_decode($result, true);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type true; however, parameter $json of json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

188
            $data = json_decode(/** @scrutinizer ignore-type */ $result, true);
Loading history...
189
190
            if (isset($data['error_description'])) {
191
                $message = $data['error_description'];
192
            } elseif (isset($data['message'])) {
193
                $message = $data['message'];
194
            }
195
196
            if (isset($data['error'])) {
197
                $message = sprintf(
198
                    '%s: %s',
199
                    $data['error'],
200
                    $message
201
                        ? (is_array($message) ? json_encode($message) : $message)
202
                        : 'Unknown'
203
                );
204
            }
205
206
            throw new PayeverCommunicationException($message, $httpCode);
207
        }
208
209
        $response = new Response();
210
211
        $response
212
            ->setRequestEntity($request->getRequestEntity())
213
            ->setResponseEntity($request->getResponseEntity())
214
            ->load($result);
215
216
        if ($response->isFailed()) {
217
            throw new PayeverCommunicationException(
218
                $response->getResponseEntity()->getErrorDescription()
219
            );
220
        }
221
222
        return $response;
223
    }
224
225
    /**
226
     * @param $fileUrl
227
     * @param $savePath
228
     * @throws \Exception
229
     */
230
    public function download($fileUrl, $savePath)
231
    {
232
        try {
233
            $this->downloadRequest($fileUrl, $savePath);
234
        } catch (\Exception $exception) {
235
            $logLevel = $this->logLevel;
236
            if (null !== $this->tmpLogLevel) {
237
                $logLevel = $this->tmpLogLevel;
238
                $this->tmpLogLevel = null;
239
            }
240
            $this->logger->log(
241
                $logLevel,
242
                sprintf(
243
                    'HTTP Request failed: %s %s',
244
                    $exception->getCode(),
245
                    $exception->getMessage()
246
                ),
247
                ['trace' => $exception->getTraceAsString()]
248
            );
249
250
            throw $exception;
251
        }
252
    }
253
254
    /**
255
     * @param $fileUrl
256
     * @param $savePath
257
     *
258
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
259
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
260
     * @SuppressWarnings(PHPMD.NPathComplexity)
261
     * @SuppressWarnings(PHPMD.ShortVariable)
262
     */
263
    protected function downloadRequest($fileUrl, $savePath)
264
    {
265
        $this->logger->debug(
266
            sprintf('HTTP download Request %s %s', $fileUrl, $savePath)
267
        );
268
269
        $filePointer = fopen($savePath, 'w+');
270
        $ch = curl_init($fileUrl);
271
272
        if ($ch === false) {
273
            throw new \RuntimeException('Could not get cURL resource');
274
        }
275
276
        $options = $this->getRequestOptions();
277
        $options[CURLOPT_TIMEOUT] = 100;
278
        $options[CURLOPT_CONNECTTIMEOUT] = 300;
279
        $options[CURLOPT_FILE] = $filePointer;
280
281
        curl_setopt_array($ch, $options);
282
283
        curl_exec($ch);
284
        $httpCode     = curl_getinfo($ch, CURLINFO_HTTP_CODE);
285
        $errorMessage = curl_error($ch);
286
        $errorNumber  = curl_errno($ch);
287
288
        curl_close($ch);
289
        fclose($filePointer);
290
291
        $this->logger->debug(
292
            sprintf('HTTP Download Response %s', $savePath),
293
            ['httpCode' => $httpCode, 'curlError' => $errorMessage]
294
        );
295
296
        if ($errorNumber !== 0) {
297
            throw new PayeverCommunicationException($errorMessage, $errorNumber);
298
        }
299
300
        if ($httpCode >= 400) {
301
            throw new PayeverCommunicationException('Could not download the file', $httpCode);
302
        }
303
    }
304
305
    /**
306
     * Returns cURL request params array
307
     *
308
     * @param array $override
309
     *
310
     * @return array
311
     */
312
    protected function getRequestOptions($override = [])
313
    {
314
        $default = [
315
            CURLOPT_HEADER         => 0,
316
            CURLOPT_RETURNTRANSFER => true,
317
            CURLOPT_SSL_VERIFYPEER => true,
318
            CURLOPT_SSL_VERIFYHOST => 2,
319
            CURLOPT_TIMEOUT        => 30,
320
            CURLOPT_CONNECTTIMEOUT => 15,
321
            CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
322
        ];
323
324
        return $override + $default;
325
    }
326
}
327