mikecbrant/php-rest-client

View on GitHub
src/RestClientLib/RestClient.php

Summary

Maintainability
C
1 day
Test Coverage
A
100%
<?php

namespace MikeBrant\RestClientLib;

/**
 * Class RestClient
 *
 * Class for executing RESTful service calls using a fluent interface.
 *
 * @package MikeBrant\RestClientLib
 */
class RestClient
{
    /**
      * Flag to determine if basic authentication is to be used.
     * 
     * @var boolean
     */
    protected $useBasicAuth = false;
    
    /**
     * User Name for HTTP Basic Auth
     * 
     * @var string
     */
    protected $basicAuthUsername = null;
    
    /**
     * Password for HTTP Basic Auth
     *
     * @var string
     */
    protected $basicAuthPassword = null;
    
    /**
     * Flag to determine if SSL is used
     * 
     * @var boolean
     */
    protected $useSsl = false;
   
    /**
     * Flag to determine is we are to run in test mode where host's SSL cert is not verified
     * 
     * @var boolean
     */
    protected $useSslTestMode = false;
    
    /**
     * Integer value representing number of seconds to set for curl timeout option. Defaults to 30 seconds.
     * 
     * @var integer
     */
    protected $timeout = 30;
    
    /**
     * Variable to store remote host name
     * 
     * @var string
     */
    protected $remoteHost = null;
    
    /**
     * Variable to hold setting to determine if redirects are followed
     * 
     * @var boolean
     */
    protected $followRedirects = false;
    
    /**
     * Variable to hold value for maximum number of redirects to follow for cases when redirect are being followed.
     * Default value of 0 will allow for following of unlimited redirects.
     * 
     * @var integer
     */
    protected $maxRedirects = 0;
    
    /**
     * Variable which can hold a URI base for all actions
     * 
     * @var string
     */
    protected $uriBase = '/';
    
    /**
     * Stores curl handle
     *
     * @var mixed
     */
    private $curl = null;
    
    /**
     * Array containing headers to be used for request
     * 
     * @var array
     */
    private $headers = array();
    
    /**
     * Variable to store the request header as sent
     * 
     * @var string
     */
    
    /**
     * Constructor method. Currently there is no instantiation logic.
     *
     * @return void
     */
    public function __construct() {}
    
    /**
     * Method to execute GET on server
     * 
     * @param string $action
     * @return CurlHttpResponse
     * @throws \InvalidArgumentException
     * @throws \Exception
     */
    public function get($action)
    {
        $this->validateAction($action);
        $this->curlSetup();
        $this->setRequestUrl($action);
        curl_setopt($this->curl, CURLOPT_HTTPGET, true);
        return $this->curlExec();
    }
    
    /**
     * Method to exexute POST on server
     * 
     * @param mixed $action
     * @param mixed $data
     * @return CurlHttpResponse
     * @throws \InvalidArgumentException
     * @throws \Exception
     */
    public function post($action, $data)
    {
        $this->validateAction($action);
        $this->validateData($data);
        $this->curlSetup();
        $this->setRequestUrl($action);
        $this->setRequestData($data);
        curl_setopt($this->curl, CURLOPT_POST, true);
        return $this->curlExec();
    }
    
    /**
     * Method to execute PUT on server
     * 
     * @param string $action
     * @param mixed $data
     * @return CurlHttpResponse
     * @throws \InvalidArgumentException
     * @throws \Exception
     */
    public function put($action, $data)
    {
        $this->validateAction($action);
        $this->validateData($data);
        $this->curlSetup();
        $this->setRequestUrl($action);
        $this->setRequestData($data);
        curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'PUT');
        return $this->curlExec();
    }
    
    /**
     * Method to execute DELETE on server
     * 
     * @param string $action
     * @return CurlHttpResponse
     * @throws \InvalidArgumentException
     * @throws \Exception
     */
    public function delete($action)
    {
        $this->validateAction($action);
        $this->curlSetup();
        $this->setRequestUrl($action);
        curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
        return $this->curlExec();
    }
    
    /**
     * Method to execute HEAD on server
     * 
     * @param string $action
     * @return CurlHttpResponse
     * @throws \InvalidArgumentException
     * @throws \Exception
     */
    public function head($action)
    {
        $this->validateAction($action);
        $this->curlSetup();
        $this->setRequestUrl($action);
        curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'HEAD');
        curl_setopt($this->curl, CURLOPT_NOBODY, true);
        return $this->curlExec();
    }
    
    /**
     * Sets host name of remote server
     * 
     * @param string $host
     * @return RestClient
     * @throws \InvalidArgumentException
     */
    public function setRemoteHost($host)
    {
        if(empty($host)) {
            throw new \InvalidArgumentException('Host name not provided.');
        } else if(!is_string($host)) {
            throw new \InvalidArgumentException('Non-string host name provided.');
        }
        
        // remove any http(s):// at beginning of host name
        $httpsPattern = '#https://#i';
        $httpPattern = '#http://#i';
        if (1 === preg_match($httpsPattern, $host)) {
            // this needs to be SSL request
            $this->setUseSsl(true);
            $host = str_ireplace('https://', '', $host);
        } else if (1 === preg_match($httpPattern, $host)) {
            $host = str_ireplace('http://', '', $host);
        }
        
        // remove trailing slash in host name
        $host = rtrim($host, '/');
        
        // look for common SSL port values in host name to see if SSL is needed
        $portPatterns = array(
            '/:443$/',
            '/:8443$/',
        );
        foreach ($portPatterns as $pattern) {
            if (1 === preg_match($pattern, $host)) {
                $this->setUseSsl(true);
            }
        }
        
        $this->remoteHost = $host;
        
        return $this;
    }
    
    /**
     * Sets URI base for the instance
     * 
     * @param string $uriBase
     * @return RestClient
     * @throws \InvalidArgumentException
     */
    public function setUriBase($uriBase)
    {
        if(empty($uriBase)) {
            throw new \InvalidArgumentException('URI base not provided.');
        } else if(!is_string($uriBase)) {
            throw new \InvalidArgumentException('Non-string URI base provided.');
        }
        
        // make sure we always have forward slash at beginning and end of uriBase
        $uriBase = '/' . ltrim($uriBase, '/');
        $uriBase = rtrim($uriBase, '/') . '/';
        $this->uriBase = $uriBase;
        
        return $this;
    }
    
    /**
     * Sets whether SSL is to be used
     * 
     * @param boolean $value
     * @return RestClient
     * @throws \InvalidArgumentException
     */
    public function setUseSsl($value)
    {
        if (!is_bool($value)) {
            throw new \InvalidArgumentException('Non-boolean value passed as parameter.');
        }
        $this->useSsl = $value;
        
        return $this;
    }
    
    /**
     * Sets whether SSL Test Mode is to be used
     * 
     * @param boolean $value
     * @return RestClient
     * @throws \InvalidArgumentException
     */
    public function setUseSslTestMode($value)
    {
        if (!is_bool($value)) {
            throw new \InvalidArgumentException('Non-boolean value passed as parameter.');
        }
        $this->useSslTestMode = $value;
        
        return $this;
    }
    /**
     * Sets basic authentication credentials
     * 
     * @param string $user
     * @param string $password
     * @return RestClient
     * @throws \InvalidArgumentException
     */
    public function setBasicAuthCredentials($user, $password)
    {
        if (empty($user)) {
            throw new \InvalidArgumentException('User name not provided when trying to set basic authentication credentials.');
        }
        if (empty($password)) {
            throw new \InvalidArgumentException('Password not provided when trying to set basic authentication credentials.');
        }
        
        $this->useBasicAuth = true;
        $this->basicAuthUsername = $user;
        $this->basicAuthPassword = $password;
        
        return $this;
    }

    /**
     * Sets HTTP headers from an associative array where key is header name and value is the header value
     * 
     * @param array $headers
     * @return RestClient
     */
    public function setHeaders(array $headers)
    {
        if(empty($headers)) {
            throw new \InvalidArgumentException('Empty array passed when triyng to set headers');
        }
        $this->headers = $headers;
        
        return $this;  
    }
    
    /**
     * Sets maximum timeout for curl requests
     * 
     * @param integer $seconds
     * @return RestClient
     * @throws \InvalidArgumentException
     */
    public function setTimeout($seconds)
    {
        if(!is_integer($seconds) || $seconds < 0) {
            throw new \InvalidArgumentException('A non-negative integer value must be passed when trying to set timeout');
        }
        $this->timeout = $seconds;
        
        return $this;
    }
    
    /**
     * Sets flag on whether to follow 3XX redirects.
     * 
     * @param boolean $follow
     * @return RestClient
     * @throws \InvalidArgumentException
     */
    public function setFollowRedirects($follow)
    {
        if(!is_bool($follow)) {
            throw new \InvalidArgumentException('Non-boolean value passed as parameter.');
        }
        $this->followRedirects = $follow;
        
        return $this;
    }
    
    /**
     * Sets maximum number of redirects to follow. A value of 0 represents no redirect limit. Also sets followRedirects property to true .
     * 
     * @param integer $redirects
     * @return RestClient
     * @throws \InvalidArgumentException
     */
    public function setMaxRedirects($redirects)
    {
        if(!is_integer($redirects) || $redirects < 0) {
            throw new \InvalidArgumentException('A non-negative integer value must be passed when trying to set max redirects.');
        }
        $this->maxRedirects = $redirects;
        $this->setFollowRedirects(true);
        
        return $this;
    }
    
    /**
     * Get remote host setting
     * 
     * @return string
     */
    public function getRemoteHost()
    {
        return $this->remoteHost;
    }
    
    /**
     * Get URI Base setting
     * 
     * @return string
     */
    public function getUriBase()
    {
        return $this->uriBase;
    }
    
    /**
     * Get boolean setting indicating whether SSL is to be used
     * 
     * @return boolean
     */
    public function isUsingSsl()
    {
        return $this->useSsl;
    }
    
    /**
     * Get boolean setting indicating whether SSL test mode is enabled
     * 
     * @return boolean
     */
    public function isUsingSslTestMode()
    {
        return $this->useSslTestMode;
    }
    
    /**
     * Get timeout setting
     * 
     * @return integer
     */
    public function getTimeout()
    {
        return $this->timeout;
    }
    
    /**
     * Get follow redirects setting
     * 
     * @return boolean
     */
    public function isFollowingRedirects()
    {
        return $this->followRedirects;
    }
    
    /**
     * Get max redirects setting
     * 
     * @return integer
     */
    public function getMaxRedirects()
    {
        return $this->maxRedirects;
    }
    
    /**
     * Method to set up curl handle on client
     * 
     * @return void
     * @throws \Exception
     */
    private function curlSetup()
    {
        $this->curl = $this->curlInit();
    }
    
    /**
     * Method to initilize and return a curl handle
     * 
     * @return resource
     * @throws \Exception
     */
    protected function curlInit()
    {
        // initialize curl
        $curl = curl_init();
        if($curl === false) {
            throw new \Exception('curl failed to initialize.');
        }
        // set timeout
        curl_setopt($curl, CURLOPT_TIMEOUT, $this->timeout);
        
        // set basic HTTP authentication settings
        if (true === $this->useBasicAuth) {
            curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
            curl_setopt($curl, CURLOPT_USERPWD, $this->basicAuthUsername . ':' . $this->basicAuthPassword);
        }
        
        // set headers
        if (!empty($this->headers)) {
            $headers = array();
            foreach ($this->headers as $key=>$val) {
                $headers[] = $key . ': ' . $val;
            }
            curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
        }
        
        // if not in production environment, we want to ignore SSL validation
        if (true === $this->useSsl && true === $this->useSslTestMode) {
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
        }
        
        // set option to add request header information to curl_getinfo output
        curl_setopt($curl, CURLINFO_HEADER_OUT, true);
        
        // set option to return content body
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        
        // set redirect options
        if (true === $this->followRedirects) {
            curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
            if ($this->maxRedirects > 0) {
                curl_setopt($curl, CURLOPT_MAXREDIRS, $this->maxRedirects);
            }
        }
        
        return $curl;
    }
    
    /**
     * Method to to teardown curl fixtures at end of request
     * 
     * @return void
     */
    private function curlTeardown()
    {
        $this->curlClose($this->curl);
        $this->curl = null;
    }
    
    /**
     * Method to close curl handle
     *
     * @param resource $curl curl handle
     * @return void
     */
    protected function curlClose($curl)
    {
        curl_close($curl);
    }
    
    /**
     * Method to execute curl call
     * 
     * @return CurlHttpResponse
     * @throws \Exception
     */
    private function curlExec()
    {
        $curlResult = curl_exec($this->curl);
        if($curlResult === false) {
            // our curl call failed for some reason
            $curlError = curl_error($this->curl);
            $this->curlTeardown();
            throw new \Exception('curl call failed with message: "' . $curlError. '"');
        }
        
        // return CurlHttpResponse
        try {
            $response = new CurlHttpResponse($curlResult, curl_getinfo($this->curl));
        } catch (\InvalidArgumentException $e) {
            throw new \Exception(
                'Unable to instantiate CurlHttpResponse. Message: "' . $e->getMessage() . '"',
                $e->getCode(),
                $e
            );
        } finally {
            $this->curlTeardown();
        }
        
        return $response;
    }
    
    /**
     * Method to set the url on curl handle based on passed action
     * 
     * @param string $action
     * @return void
     */
    protected function setRequestUrl($action)
    {
        $url = $this->buildUrl($action);
        curl_setopt($this->curl, CURLOPT_URL, $url);
    }
    
    /**
     * Method to build URL based on class settings and passed action
     * 
     * @param string $action
     * @return string
     */
    protected function buildUrl($action)
    {
        $url = 'http://';
        if (true === $this->useSsl) {
            $url = 'https://';
        }
        $url = $url . $this->remoteHost . $this->uriBase . $action;
        return $url;
    }
    
    /**
     * Method to set data to be sent along with POST/PUT requests
     * 
     * @param mixed $data
     * @return void
     */
    protected function setRequestData($data)
    {
        curl_setopt($this->curl, CURLOPT_POSTFIELDS, $data);
    }
    
    /**
     * Method to provide common validation for action parameters
     * 
     * @param string $action
     * @return void
     * @throws \InvalidArgumentException
     */
    protected function validateAction($action)
    {
        if(!is_string($action)) {
            throw new \InvalidArgumentException('A non-string value was passed for action parameter');
        }
    }
    
    /**
     * Method to provide common validation for data parameters
     * 
     * @param mixed $data
     * @return void
     * @throws \InvalidArgumentException
     */
    protected function validateData($data)
    {
        if(empty($data)) {
            throw new \InvalidArgumentException('An empty value was passed for data parameter');
        }
    }
}