dadajuice/zephyrus

View on GitHub
src/Zephyrus/Network/HttpRequester.php

Summary

Maintainability
B
4 hrs
Test Coverage
B
87%
<?php namespace Zephyrus\Network;

use CURLFile;
use CurlHandle;
use InvalidArgumentException;
use Zephyrus\Exceptions\HttpRequesterException;
use Zephyrus\Network\HttpRequester\HttpPayload;
use Zephyrus\Network\HttpRequester\HttpRequesterResponse;
use Zephyrus\Network\HttpRequester\RequestTypes\HttpDownloadRequest;
use Zephyrus\Network\HttpRequester\RequestTypes\HttpStreamRequest;
use Zephyrus\Network\HttpRequester\RequestTypes\HttpUploadRequest;
use Zephyrus\Utilities\FileSystem\File;

class HttpRequester
{
    private string $url;
    private string $method;
    private bool $verifySsl = true;
    private bool $followRedirection = true;
    private string $userAgent = "Zephyrus HTTP Requester/1.0.0";
    private array $options = [];
    private array $headers = [];
    private string $contentType = ContentType::FORM;
    private int $connectionTimeout = 15;
    private string $accept = ContentType::ANY;
    private $writeCallback;

    use HttpStreamRequest;
    use HttpDownloadRequest;
    use HttpUploadRequest;

    public static function get(string $url): self
    {
        return new self('get', $url);
    }

    public static function post(string $url): self
    {
        return new self('post', $url);
    }

    public static function put(string $url): self
    {
        return new self('put', $url);
    }

    public static function patch(string $url): self
    {
        return new self('patch', $url);
    }

    public static function delete(string $url): self
    {
        return new self('delete', $url);
    }

    /**
     * Returns a compatible CURLFile instance for the given local file (complete path) which can then be used with the
     * executeUpload method. If no uploadFilename is given, the original filename will be used (including extension).
     *
     * @param string $localFilePath
     * @param string|null $uploadFilename
     * @return CURLFile
     */
    public static function prepareUploadFile(string $localFilePath, ?string $uploadFilename = null): CURLFile
    {
        if (!is_readable($localFilePath)) {
            throw new InvalidArgumentException("Specified filepath [$localFilePath] is not readable and thus cannot be prepared as a remote request file transfer");
        }
        return (new File($localFilePath))->buildCurlFile($uploadFilename);
    }

    public function __construct(string $method, string $url)
    {
        $this->method = strtolower($method);
        $this->url = $url;
    }

    /**
     * Executes the HTTP request with the given payload. The payload can either be an array for the form content type
     * e.g. application/x-www-form-urlencoded or a string for other type such as application/json. If the payload is
     * an array and contains a CURLFile, it will automatically be handled as an upload attempt and switch the content
     * type to multipart/form-data.
     *
     * @param array|string $payload
     * @throws HttpRequesterException
     * @return HttpRequesterResponse
     */
    public function execute(string|array $payload = ""): HttpRequesterResponse
    {
        $responseHeaders = [];
        $curl = $this->buildCurl(new HttpPayload($this->contentType, $payload));
        curl_setopt($curl, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
            $len = strlen($header);
            $header = explode(':', $header, 2);
            if (count($header) < 2) {
                return $len;
            }
            $responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
            return $len;
        });
        $response = curl_exec($curl);
        if ($response === false) {
            throw new HttpRequesterException(curl_error($curl), $this->method, $this->url);
        }
        $information = curl_getinfo($curl);
        curl_close($curl);
        return new HttpRequesterResponse($response, $information, $responseHeaders);
    }

    /**
     * Adds an HTTP header to the request. If the header already exists, it will update its value.
     *
     * @param string $name
     * @param string $value
     */
    public function addHeader(string $name, string $value)
    {
        $this->headers[$name] = $value;
    }

    /**
     * Adds a list of HTTP header to the request. Array must be associative where the keys are the header name.
     *
     * @param array $headers
     */
    public function addHeaders(array $headers)
    {
        foreach ($headers as $name => $value) {
            $this->headers[$name] = $value;
        }
    }

    /**
     * Removes the specified name from the headers list. Nothing is made if the header is already not present.
     *
     * @param string $name
     */
    public function removeHeader(string $name)
    {
        if (isset($this->headers[$name])) {
            unset($this->headers[$name]);
        }
    }

    /**
     * Removes a list of specified names from the headers list. Nothing is made if the header is already not present.
     *
     * @param array $names
     */
    public function removeHeaders(array $names)
    {
        foreach ($names as $name) {
            $this->removeHeader($name);
        }
    }

    /**
     * Adds a cURL custom option to the request.
     *
     * @param string $curlOption
     * @param mixed $value
     */
    public function addOption(string $curlOption, mixed $value)
    {
        $this->options[$curlOption] = $value;
    }

    /**
     * Adds a list of cURL custom options to the request. Array must be associative where the keys are the option name.
     *
     * @param array $curlOptions
     */
    public function addOptions(array $curlOptions)
    {
        foreach ($curlOptions as $curlOption => $value) {
            $this->options[$curlOption] = $value;
        }
    }

    /**
     * Removes the specified cURL option.
     *
     * @param string $curlOption
     */
    public function removeOption(string $curlOption)
    {
        if (isset($this->options[$curlOption])) {
            unset($this->options[$curlOption]);
        }
    }

    /**
     * Removes a list of specified cURL options.
     *
     * @param array $curlOptions
     */
    public function removeOptions(array $curlOptions)
    {
        foreach ($curlOptions as $option) {
            $this->removeOption($option);
        }
    }

    /**
     * Defines the connection timeout in seconds. Defaults to 15 seconds.
     *
     * @param int $connectionTimeout
     */
    public function setConnectionTimeout(int $connectionTimeout)
    {
        $this->connectionTimeout = $connectionTimeout;
    }

    /**
     * Defines if the requester should follow redirections the destination server returns. Defaults to true.
     *
     * @param bool $followRedirection
     */
    public function setFollowRedirection(bool $followRedirection)
    {
        $this->followRedirection = $followRedirection;
    }

    /**
     * Disable or enable the SSL verification. Should always be enabled in production environnement. Disabling is
     * useful when using a self-signed certificate. Handles the CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST
     * options.
     *
     * @param bool $verifySsl
     * @return void
     */
    public function setSslVerification(bool $verifySsl)
    {
        $this->verifySsl = $verifySsl;
    }

    /**
     * Defines the requester user agent to use for the request.
     *
     * @param string $userAgent
     */
    public function setUserAgent(string $userAgent)
    {
        $this->userAgent = $userAgent;
    }

    public function setWriteCallback(callable $callback)
    {
        $this->writeCallback = $callback;
    }

    /**
     * Defines the content type used to send the request. Defaults to application/x-www-form-urlencoded.
     *
     * @param string $contentType
     */
    public function setContentType(string $contentType)
    {
        $this->contentType = $contentType;
    }

    /**
     * Prepares the cURL instance based on the requester configurations.
     *
     * @param HttpPayload $payload
     * @return CurlHandle
     */
    private function buildCurl(HttpPayload $payload): CurlHandle
    {
        $this->addHeader('Content-Type', $payload->getContentType());
        $curl = curl_init();
        $this->setDefaultHeaders();
        $this->setCurlBasicOptions($curl);
        $this->setCurlUserAgent($curl);
        $this->setCurlCustomMethod($curl);
        $this->setCurlSsl($curl);
        $this->setCurlWriteFunction($curl);
        $this->setCurlAdditionalOptions($curl);
        $this->setCurlHeaders($curl);
        $this->setCurlPayload($curl, $payload);
        return $curl;
    }

    /**
     * Applies the specified Accept and any other default headers.
     */
    private function setDefaultHeaders()
    {
        if (!empty($this->accept)) {
            $this->addHeader('Accept', $this->accept);
        }
    }

    /**
     * Applies basic cURL options including connection timeout, redirection directive, etc.
     *
     * @param CurlHandle $curl
     */
    private function setCurlBasicOptions(CurlHandle $curl)
    {
        curl_setopt($curl, CURLOPT_URL, $this->url);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HEADER, 0);
        curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $this->connectionTimeout);
        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, $this->followRedirection);
        curl_setopt($curl, CURLINFO_HEADER_OUT, true);
    }

    /**
     * Applies the proper request method to the cURL CUSTOMREQUEST option is needed. All method besides get and post
     * are considered "custom" to the cURL instance.
     *
     * @param CurlHandle $curl
     */
    private function setCurlCustomMethod(CurlHandle $curl)
    {
        if ($this->method != 'get' && $this->method != 'post') {
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, strtoupper($this->method));
        }
    }

    /**
     * Applies the user agent option to the cURL instance.
     *
     * @param CurlHandle $curl
     */
    private function setCurlUserAgent(CurlHandle $curl)
    {
        if (!empty($this->userAgent)) {
            curl_setopt($curl, CURLOPT_USERAGENT, $this->userAgent);
        }
    }

    private function setCurlWriteFunction(CurlHandle $curl)
    {
        if (!is_null($this->writeCallback)) {
            curl_setopt($curl, CURLOPT_WRITEFUNCTION, $this->writeCallback);
        }
    }

    /**
     * Applies the SSL verification options to the cURL instance.
     *
     * @param CurlHandle $curl
     */
    private function setCurlSsl(CurlHandle $curl)
    {
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $this->verifySsl ? 2 : 0);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verifySsl ? 2 : 0);
    }

    /**
     * Applies any other specified cURL options.
     *
     * @param CurlHandle $curl
     */
    private function setCurlAdditionalOptions(CurlHandle $curl)
    {
        foreach ($this->options as $curlOption => $value) {
            curl_setopt($curl, $curlOption, $value);
        }
    }

    /**
     * Applies the specified Content-Type, Accept and any other given headers to the cURL HTTPHEADER option using the
     * proper required formatting by cURL instance.
     *
     * @param CurlHandle $curl
     */
    private function setCurlHeaders(CurlHandle $curl)
    {
        $curlCompatibleHeaders = [];
        foreach ($this->headers as $name => $value) {
            $curlCompatibleHeaders[] = "$name:$value";
        }
        curl_setopt($curl, CURLOPT_HTTPHEADER, $curlCompatibleHeaders);
    }

    /**
     * Applies the given payload into the cURL POSTFIELDS option. If the payload registered content type is
     * application/x-www-form-urlencoded, it will proceed to properly encode the query. Otherwise, it will be sent
     * as is.
     *
     * @param CurlHandle $curl
     * @param HttpPayload $payload
     */
    private function setCurlPayload(CurlHandle $curl, HttpPayload $payload)
    {
        if (empty($payload->getContent())) {
            return;
        }
        if ($this->method == 'get') {
            $requestedUrl = $this->url . (!str_contains($this->url, '?') ? '?' : '&') . $payload->getContent();
            curl_setopt($curl, CURLOPT_URL, $requestedUrl);
            return;
        }
        curl_setopt($curl, CURLOPT_POST, true);
        curl_setopt($curl, CURLOPT_POSTFIELDS, $payload->getContent());
    }
}