Completed
Push — master ( 8f9c73...f95db6 )
by WEBEWEB
02:15
created

AbstractCURLRequest::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
1
<?php
2
3
/**
4
 * This file is part of the core-library package.
5
 *
6
 * (c) 2018 WEBEWEB
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace WBW\Library\Core\Network\CURL\Request;
13
14
use DateTime;
15
use WBW\Library\Core\Argument\ArgumentHelper;
16
use WBW\Library\Core\Exception\IO\InvalidHTTPMethodException;
17
use WBW\Library\Core\Exception\Network\CURLRequestCallException;
18
use WBW\Library\Core\Network\CURL\Configuration\CURLConfiguration;
19
use WBW\Library\Core\Network\CURL\Response\CURLResponse;
20
use WBW\Library\Core\Network\HTTP\HTTPInterface;
21
22
/**
23
 * Abstract cURL request.
24
 *
25
 * @author webeweb <https://github.com/webeweb/>
26
 * @package WBW\Library\Core\Network\CURL\Request
27
 * @abstract
28
 */
29
abstract class AbstractCURLRequest implements CURLRequestInterface, HTTPInterface {
30
31
    /**
32
     * Content-type application/x-www-form-urlencoded
33
     *
34
     * @var string
35
     */
36
    const CONTENT_TYPE_X_WWW_FORM_URLENCODED = "Content-Type: application/x-www-form-urlencoded";
37
38
    /**
39
     * Configuration.
40
     *
41
     * @var CURLConfiguration
42
     */
43
    private $configuration;
44
45
    /**
46
     * Headers.
47
     *
48
     * @var array
49
     */
50
    private $headers = [];
51
52
    /**
53
     * Method.
54
     *
55
     * @var string
56
     */
57
    private $method = self::HTTP_METHOD_GET;
58
59
    /**
60
     * POST data.
61
     *
62
     * @var array
63
     */
64
    private $postData = [];
65
66
    /**
67
     * Query data.
68
     *
69
     * @var array
70
     */
71
    private $queryData = [];
72
73
    /**
74
     * Resource path.
75
     *
76
     * @var string
77
     */
78
    private $resourcePath;
79
80
    /**
81
     * Constructor.
82
     *
83
     * @param string $method The Method.
84
     * @param CURLConfiguration $configuration The configuration.
85
     * @param string $resourcePath The resource path.
86
     * @throws InvalidHTTPMethodException Throws an invalid HTTP method exception if the method is not implemented.
87
     */
88
    protected function __construct($method, CURLConfiguration $configuration, $resourcePath) {
89
        $this->setConfiguration($configuration);
90
        $this->setMethod($method);
91
        $this->setResourcePath($resourcePath);
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97
    public function addHeader($name, $value) {
98
        ArgumentHelper::isTypeOf($name, ArgumentHelper::ARGUMENT_STRING);
99
        $this->headers[$name] = $value;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105
    public function addPostData($name, $value) {
106
        ArgumentHelper::isTypeOf($name, ArgumentHelper::ARGUMENT_STRING);
107
        $this->postData[$name] = $value;
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113
    public function addQueryData($name, $value) {
114
        ArgumentHelper::isTypeOf($name, ArgumentHelper::ARGUMENT_STRING);
115
        $this->queryData[$name] = $value;
116
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121
    public function call() {
122
123
        // Define the necessary argurments.
124
        $curlHeaders  = $this->mergeHeaders();
125
        $curlPOSTData = http_build_query($this->getPostData());
126
127
        //
128
        if (true === in_array("Content-Type: application/json", $curlHeaders)) {
129
            $curlPOSTData = json_encode($this->getPostData());
130
        }
131
132
        // Initialize the URL.
133
        $curlURL = $this->mergeURL();
134
        if (0 < count($this->getQueryData())) {
135
            $curlURL = implode("?", [$curlURL, http_build_query($this->getQueryData())]);
136
        }
137
138
        // Initialize cURL.
139
        $stream = curl_init();
140
141
        // Set the connect timeout.
142
        if (0 < $this->getConfiguration()->getConnectTimeout()) {
143
            curl_setopt($stream, CURLOPT_CONNECTTIMEOUT, $this->getConfiguration()->getConnectTimeout());
144
        }
145
146
        // Set the encoding.
147
        if (true === $this->getConfiguration()->getAllowEncoding()) {
148
            curl_setopt($stream, CURLOPT_ENCODING, "");
149
        }
150
151
        // Set the HTTP headers.
152
        curl_setopt($stream, CURLOPT_HTTPHEADER, $curlHeaders);
153
154
        // Set the post.
155
        switch ($this->getMethod()) {
156
157
            case self::HTTP_METHOD_DELETE:
158
            case self::HTTP_METHOD_OPTIONS:
159
            case self::HTTP_METHOD_PATCH:
160
            case self::HTTP_METHOD_PUT:
161
                curl_setopt($stream, CURLOPT_CUSTOMREQUEST, $this->getMethod());
162
                curl_setopt($stream, CURLOPT_POSTFIELDS, $curlPOSTData);
163
                break;
164
165
            case self::HTTP_METHOD_HEAD:
166
                curl_setopt($stream, CURLOPT_NOBODY, true);
167
                break;
168
169
            case self::HTTP_METHOD_POST:
170
                curl_setopt($stream, CURLOPT_POST, true);
171
                curl_setopt($stream, CURLOPT_POSTFIELDS, $curlPOSTData);
172
                break;
173
        }
174
175
        // Set the proxy.
176
        if (null !== $this->getConfiguration()->getProxyHost()) {
177
            curl_setopt($stream, CURLOPT_PROXY, $this->getConfiguration()->getProxyHost());
178
        }
179
        if (null !== $this->getConfiguration()->getProxyPort()) {
180
            curl_setopt($stream, CURLOPT_PROXYPORT, $this->getConfiguration()->getProxyPort());
181
        }
182
        if (null !== $this->getConfiguration()->getProxyType()) {
183
            curl_setopt($stream, CURLOPT_PROXYTYPE, $this->getConfiguration()->getProxyType());
184
        }
185
        if (null !== $this->getConfiguration()->getProxyUsername()) {
186
            curl_setopt($stream, CURLOPT_PROXYUSERPWD, implode(":", [$this->getConfiguration()->getProxyUsername(), $this->getConfiguration()->getProxyPassword()]));
187
        }
188
189
        // Set the return.
190
        curl_setopt($stream, CURLOPT_RETURNTRANSFER, true);
191
192
        // Set the SSL verification.
193
        if (false === $this->getConfiguration()->getSslVerification()) {
194
            curl_setopt($stream, CURLOPT_SSL_VERIFYHOST, 0);
195
            curl_setopt($stream, CURLOPT_SSL_VERIFYPEER, 0);
196
        }
197
198
        // Set the request timeout.
199
        if (0 < $this->getConfiguration()->getRequestTimeout()) {
200
            curl_setopt($stream, CURLOPT_TIMEOUT, $this->getConfiguration()->getRequestTimeout());
201
        }
202
203
        // Set the URL.
204
        curl_setopt($stream, CURLOPT_URL, $curlURL);
205
206
        // Set the user agent.
207
        curl_setopt($stream, CURLOPT_USERAGENT, $this->getConfiguration()->getUserAgent());
208
209
        // Get the HTTP response headers.
210
        curl_setopt($stream, CURLOPT_HEADER, 1);
211
212
        // Set the verbose.
213
        if (true === $this->getConfiguration()->getDebug()) {
214
            curl_setopt($stream, CURLOPT_STDERR, fopen($this->getConfiguration()->getDebugFile(), "a"));
215
            curl_setopt($stream, CURLOPT_VERBOSE, 0);
216
217
            $msg = (new DateTime())->format("c") . " [DEBUG] " . $curlURL . PHP_EOL . "HTTP request body ~BEGIN~" . PHP_EOL . print_r($curlPOSTData, true) . PHP_EOL . "~END~" . PHP_EOL;
218
            error_log($msg, 3, $this->getConfiguration()->getDebugFile());
219
        } else {
220
            if (true === $this->getConfiguration()->getVerbose()) {
221
                curl_setopt($stream, CURLOPT_VERBOSE, 1);
222
            } else {
223
                curl_setopt($stream, CURLOPT_VERBOSE, 0);
224
            }
225
        }
226
227
        // Make the request.
228
        $curlExec     = curl_exec($stream);
229
        $httpHeadSize = curl_getinfo($stream, CURLINFO_HEADER_SIZE);
230
        $httpHead     = $this->parseheader(substr($curlExec, 0, $httpHeadSize));
231
        $httpBody     = substr($curlExec, $httpHeadSize);
232
        $curlInfo     = curl_getinfo($stream);
233
234
        //
235
        if (true === $this->getConfiguration()->getDebug()) {
236
            $msg = (new DateTime())->format("c") . " [DEBUG] " . $curlURL . PHP_EOL . "HTTP response body ~BEGIN~" . PHP_EOL . print_r($httpBody, true) . PHP_EOL . "~END~" . PHP_EOL;
237
            error_log($msg, 3, $this->getConfiguration()->getDebugFile());
238
        }
239
240
        // Initialize the response.
241
        $response = new CURLResponse();
242
        $response->setRequestBody($curlPOSTData);
243
        $response->setRequestHeader($curlHeaders);
244
        $response->setRequestURL($curlURL);
245
        $response->setResponseBody($httpBody);
246
        $response->setResponseHeader($httpHead);
247
        $response->setResponseInfo($curlInfo);
248
249
        // Check HTTP code.
250
        if (200 <= $curlInfo["http_code"] && $curlInfo["http_code"] <= 299) {
251
252
            // Return the response.
253
            return $response;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $response; (WBW\Library\Core\Network...L\Response\CURLResponse) is incompatible with the return type declared by the interface WBW\Library\Core\Network...LRequestInterface::call of type WBW\Library\CURL\Response\CURLResponseInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
254
        }
255
256
        // Initialize the parameters.
257
        $cde = $curlInfo["http_code"];
258
        $msg = curl_errno($stream);
259
260
        // Check the HTTP code.
261
        if (0 === $curlInfo["http_code"]) {
262
            if (false === empty(curl_error($stream))) {
263
                $msg = "Call to " . $curlURL . " failed : " . curl_error($stream);
264
            } else {
265
                $msg = "Call to " . $curlURL . " failed, but for an unknown reason. This could happen if you are disconnected from the network.";
266
            }
267
        }
268
269
        // Throw the exception.
270
        throw new CURLRequestCallException($msg, $cde, $response);
271
    }
272
273
    /**
274
     * {@inheritdoc}
275
     */
276
    public function clearHeaders() {
277
        return $this->setHeaders();
278
    }
279
280
    /**
281
     * {@inheritdoc}
282
     */
283
    public function clearPostData() {
284
        return $this->setPostData();
285
    }
286
287
    /**
288
     * {@inheritdoc}
289
     */
290
    public function clearQueryData() {
291
        return $this->setQueryData();
292
    }
293
294
    /**
295
     * {@inheritdoc}
296
     */
297
    public function getConfiguration() {
298
        return $this->configuration;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->configuration; (WBW\Library\Core\Network...ation\CURLConfiguration) is incompatible with the return type declared by the interface WBW\Library\Core\Network...rface::getConfiguration of type WBW\Library\CURL\Configuration\CURLConfiguration.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304
    public function getHeaders() {
305
        return $this->headers;
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311
    public function getMethod() {
312
        return $this->method;
313
    }
314
315
    /**
316
     * {@inheritdoc}
317
     */
318
    public function getPostData() {
319
        return $this->postData;
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     */
325
    public function getQueryData() {
326
        return $this->queryData;
327
    }
328
329
    /**
330
     * {@inheritdoc}
331
     */
332
    public function getResourcePath() {
333
        return $this->resourcePath;
334
    }
335
336
    /**
337
     * Merge the headers.
338
     *
339
     * @return array Returns the meged headers.
340
     */
341
    private function mergeHeaders() {
342
343
        // Initialize the merged headers.
344
        $mergedHeaders = [];
345
346
        // Handle each header.
347
        foreach (array_merge($this->getConfiguration()->getHeaders(), $this->getHeaders()) as $key => $value) {
348
            $mergedHeaders[] = implode(": ", [$key, $value]);
349
        }
350
351
        // Return the merged headers.
352
        return $mergedHeaders;
353
    }
354
355
    /**
356
     * Merge the URL.
357
     *
358
     * @return string Returns the merged URL.
359
     */
360
    private function mergeURL() {
361
362
        // Initialize the merged URL.
363
        $mergedURL   = [];
364
        $mergedURL[] = $this->getConfiguration()->getHost();
365
        if (null !== $this->getResourcePath() && "" !== $this->getResourcePath()) {
366
            $mergedURL[] = $this->getResourcePath();
367
        }
368
369
        // Return the merged URL.
370
        return implode("/", $mergedURL);
371
    }
372
373
    /**
374
     * Parse the raw header.
375
     *
376
     * @param string $rawHeader The raw header.
377
     * @return array Returns the headers.
378
     */
379
    private function parseHeader($rawHeader) {
380
381
        // Initialize the headers.
382
        $headers = [];
383
        $key     = "";
384
385
        // Handle each header.
386
        foreach (explode("\n", $rawHeader) as $h) {
387
            $h = explode(":", $h, 2);
388
            if (true === isset($h[1])) {
389
                if (false === isset($headers[$h[0]])) {
390
                    $headers[$h[0]] = trim($h[1]);
391
                } elseif (true === is_array($headers[$h[0]])) {
392
                    $headers[$h[0]] = array_merge($headers[$h[0]], [trim($h[1])]);
393
                } else {
394
                    $headers[$h[0]] = array_merge([$headers[$h[0]]], [trim($h[1])]);
395
                }
396
                $key = $h[0];
397
            } else {
398
                if ("\t" === substr($h[0], 0, 1)) {
399
                    $headers[$key] .= "\r\n\t" . trim($h[0]);
400
                } elseif (!$key) {
401
                    $headers[0] = trim($h[0]);
402
                }
403
                trim($h[0]);
404
            }
405
        }
406
407
        // Return the headers.
408
        return $headers;
409
    }
410
411
    /**
412
     * {@inheritdoc}
413
     */
414
    public function removeHeader($name) {
415
        if (true === array_key_exists($name, $this->headers)) {
416
            unset($this->headers[$name]);
417
        }
418
        return $this;
419
    }
420
421
    /**
422
     * {@inheritdoc}
423
     */
424
    public function removePostData($name) {
425
        if (true === array_key_exists($name, $this->postData)) {
426
            unset($this->postData[$name]);
427
        }
428
        return $this;
429
    }
430
431
    /**
432
     * {@inheritdoc}
433
     */
434
    public function removeQueryData($name) {
435
        if (true === array_key_exists($name, $this->queryData)) {
436
            unset($this->queryData[$name]);
437
        }
438
        return $this;
439
    }
440
441
    /**
442
     * Set the configuration.
443
     *
444
     * @param CURLConfiguration $configuration The configuration.
445
     * @return AbstractCURLRequest Returns this request.
446
     */
447
    protected function setConfiguration(CURLConfiguration $configuration) {
448
        $this->configuration = $configuration;
449
        return $this;
450
    }
451
452
    /**
453
     * Set the headers.
454
     *
455
     * @param array $headers The headers.
456
     * @return AbstractCURLRequest Returns this request.
457
     */
458
    protected function setHeaders(array $headers = []) {
459
        $this->headers = $headers;
460
        return $this;
461
    }
462
463
    /**
464
     * Set the method.
465
     *
466
     * @param string $method The method.
467
     * @return AbstractCURLRequest Returns this request.
468
     * @throws InvalidHTTPMethodException Throws an invalid HTTP method exception if the method is not implemented.
469
     */
470
    protected function setMethod($method) {
471
        switch ($method) {
472
            case self::HTTP_METHOD_DELETE:
473
            case self::HTTP_METHOD_GET:
474
            case self::HTTP_METHOD_HEAD:
475
            case self::HTTP_METHOD_OPTIONS:
476
            case self::HTTP_METHOD_PATCH:
477
            case self::HTTP_METHOD_POST:
478
            case self::HTTP_METHOD_PUT:
479
                $this->method = $method;
480
                break;
481
            default:
482
                throw new InvalidHTTPMethodException($method);
483
        }
484
        return $this;
485
    }
486
487
    /**
488
     * Set the POST data.
489
     *
490
     * @param array $postData The POST data.
491
     * @return AbstractCURLRequest Returns this request.
492
     */
493
    protected function setPostData(array $postData = []) {
494
        $this->postData = $postData;
495
        return $this;
496
    }
497
498
    /**
499
     * Set the query data.
500
     *
501
     * @param array $queryData The query data.
502
     * @return AbstractCURLRequest Returns this request.
503
     */
504
    protected function setQueryData(array $queryData = []) {
505
        $this->queryData = $queryData;
506
        return $this;
507
    }
508
509
    /**
510
     * {@inheritdoc}
511
     */
512
    public function setResourcePath($resourcePath) {
513
        $this->resourcePath = preg_replace("/^\//", "", trim($resourcePath));
514
        return $this;
515
    }
516
517
}
518