mgribov/apiscrape

View on GitHub
src/Scrape/Client/HttpClient.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace Scrape\Client;

/**
 * Client to build, send, and receive HTTP and convert JSON response to array
 */
class HttpClient implements HttpClientInterface {

    protected static $HTTP_CODE_OK = array(200, 304);

    protected $storage;
    protected $auth;    
    protected $debug;
    protected $timeout = 10;
    
    public $ret = [];
    public $req = [];
    
    public function setAuth(\Scrape\Auth\AuthInterface $auth) {
        $this->auth = $auth;
    }
    
    public function getAuth() {
        return $this->auth;
    }

    /**
     * Local storage for responses to allow respecting cache/etags
     * 
     * @param \Scrape\Storage\HttpStorageInterface $storage 
     */
    public function setStorage(\Scrape\Storage\HttpStorageInterface $storage) {
        $this->storage = $storage;
    }
    
    public function getStorage() {
        return $this->storage;
    }
    
    public function setTimeout($sec) {
        $this->timeout = $sec;
    }
    
    public function getTimeout() {
        return $this->timeout;
    }
    
    /**
     *
     * @param bool $v 
     */
    public function setDebug($v) {
        $this->debug = $v;
    }


    /**
     * Performs curl request against given URL with optional params and optional method
     * 
     * @param string $path - full path before any params 
     * @param array $params - associative array
     * @param string $method - default is GET
     * @return array - exact return from API as an object
     * @throws \Exception 
     */
    public function request($path, $params = array(), $method = 'GET', $headers = array()) {
        $path_get = $path;
        $etag = null;
        $current = false;
                
        // determine how to handle auth for this request
        // 
        // @todo ultimately, this breaks need for an AuthInterface
        // would have to hardcode other class handling here
        if ($this->auth instanceof \Scrape\Auth\HttpBasic) {
            $headers[] = "Authorization: Basic " . base64_encode($this->auth->getUser() . ':' . $this->auth->getSecret());
            
        } elseif ($this->auth instanceof \Scrape\Auth\Url) {
            
            if ($this->auth->getUserField() && $this->auth->getUser()) {
                $params[$this->auth->getUserField()] = $this->auth->getUser(); 
            }
            
            if ($this->auth->getSecretField() && $this->auth->getSecret()) {
                $params[$this->auth->getSecretField()] = $this->auth->getSecret();
            }
        }

        // for GETs check local storage first
        if ($this->storage && $method == 'GET') {
            $par = http_build_query($params);

            if (strlen($par)) {
                if (preg_match('/\?/', $path)) {
                    $path_get .= '&' . $par;
                } else {
                    $path_get .= '?' . $par;
                }
            }

            $current = $this->storage->get($path_get);
            
            if ($current) {
                
                if ($this->storage->isCurrent()) {
                    $this->__debug($path_get . ': found current copy, serving from cache');
                    return $this->storage->getResponse();
                } 

                $etag = $this->storage->getEtag();

                if ($etag) {
                    $this->__debug($path_get . ': found current etag will try to use it');
                    $headers[] = 'If-None-Match: "' . $etag . '"';
                }
            }
        }

        $res = $this->__curl($path, $method, $params, $headers);
        $response =  json_decode($res['body'], true);

        if (strlen($res['error']) || !in_array($res['code'], self::$HTTP_CODE_OK)) {
            $this->__debug($path . ": cURL did not return a success code: {$res['code']} {$res['error']}");

        } elseif (in_array($res['code'], self::$HTTP_CODE_OK) && $this->storage && $method == 'GET') {            

            // @todo may be wrap this all in 1 call in HttpStorage
            switch ($res['code']) {
                case 200:
                    $this->__debug("got 200 for $path_get, saving locally");
                    $this->storage->save($path_get, $response, $res['header']);
                    break;

                // 304 will return empty data from server, so load object from storage and bump its cache timer
                case 304:
                    $this->__debug("got 304 for $path_get, bumping local cache timer");
                    $this->storage->bumpCache($path_get);
                    $response = $this->storage->getResponse();
                    break;
            }
                    
        }

        return $response;

    }

    /**
     * cURL wrapper
     * 
     * @param string $path
     * @param string $method
     * @param array $headers
     * @return array 
     */
    protected function __curl($path, $method = 'GET', array $params = array(), array $headers = array()) {
        $this->ret = [];
        
        // only JSON for now
        $headers[] = 'Content-Type: application/json';
        $headers[] = 'Accept: application/json';

        $options = array();
        $options[CURLOPT_RETURNTRANSFER] = true;
        $options[CURLOPT_HEADER] = true;
        $options[CURLOPT_TIMEOUT] = $this->timeout;
        $options[CURLOPT_CUSTOMREQUEST] = $method;
        $options[CURLOPT_HTTPHEADER] = $headers;

        if ($method == 'POST') {
            // not doing this, since we are json-only for now
            //$options[CURLOPT_POST] = 1;

            $options[CURLOPT_POSTFIELDS] = json_encode($params);            

        } elseif ($method == 'GET') {
            $par = http_build_query($params);

            if (strlen($par)) {
                if (preg_match('/\?/', $path)) {
                    $path .= '&' . $par;
                } else {
                    $path .= '?' . $par;
                }
            }

        }

        $options[CURLOPT_URL] = $path;

        $this->req = [
            'path' => $path, 
            'method' => $method, 
            'headers' => $headers, 
            'options' => $options,
        ];
        
        $curl = curl_init();
        curl_setopt_array($curl, $options); 
        
        $resp = curl_exec($curl);
        $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
        $this->ret['header'] = substr($resp, 0, $header_size);
        $this->ret['body'] = substr($resp, $header_size);        
        $this->ret['error'] = curl_error($curl);
        $this->ret['code'] = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);
        
        return $this->ret;        
        
    }

    /**
     *
     * @param mixed $data 
     */
    public function __debug($data) {
        if ($this->debug) {
            var_dump($data);
        }
    }

}