<?php
/**
 * api: php7
 * title: fluent curl
 * description: simple wrapper around curl functions
 * version: 0.3
 * license: Public Domain
 *
 *
 * This simple wrapper class and its factory function allow
 * compact invocations of curl. You can pass either just an
 * URL, an option array, a previously wrapped curl() or a
 * raw curl resource handle.
 * Methods and options (CURL_ constants) lose their prefix
 * once wrapped, and can be easily chained:
 *
 *   curl($url)->followlocation(1)->verbose(1)->exec();
 *
 * Option names are case-insensitive too. The exec() and any
 * setting retrieval terminate the fluent call.
 *
 *
 * Individual curl() wrappers can also be combined into a
 * parallel-request curl_multi() instance:
 *
 *   curl_multi($url1, $url2, $url3)->verbose(1)->exec();
 *
 * Here the handles may be passed as associative array or
 * indexed list. exec() will wait until all finish, then
 * directly return an array of result contents.
 * In-between option calls are applied to all bound handles.
 *
 */



# hybrid constructor
function curl($url=NULL) {
    return is_a($url, "curl") ? $url : new curl($url);
}



/**
 * Fluent curl wrapper which obviates CURL_ prefixes.
 *
 * Makes curl_functions available as methods. Previous constants
 * are easily accessible through chainable method calls as well.
 * Only getinfo() options are mapped as virtual properties.
 *
 */
class curl {


    /**
     * resource / curl handle
     *
     */
    public $handle = NULL;
    
    
    /**
     * Setup parameters.
     *
     */
    static $defaults = array(
        "returntransfer" => 1,
        "httpget" => 1,
        "http200aliases" => array(200, 201, 202, 203),
        "header" => 0,
    );


    /**
     * Some checks before returning the resulting content.
     *
     */
    public $assert = array();


    /**
     * Initialize, where $params is usually just an $url, or an [OPT=>val] list
     *
     */
    function __construct($params) {
    
        // create
        $this->handle = is_object($params) ? $params : curl_init();
        
        // merge options
        $params = array_merge(
            curl::$defaults,
            array("followlocation" => !ini_get("safe_mode")),
            is_array($params) ? array_change_key_case($params) : array(),
            is_string($params) ? array("url" => $params) : array()
        );

        // multiple [url=>$url, post=>$bool, ..] options
        if (is_array($params)) foreach ($params as $cmd=>$opt) {
            $this->__call($cmd, array($opt));
        }
    }

    
    /**
     * Most invocations are mapped onto CURL_CONST settings,
     * while some are just plain function calls:
     *
     * @method exec()
     * @method errno()
     * @method error()
     * @method getinfo()
     */
    function __call($opt, $args) {

        # CURLOPT_*** constant
        if (defined($const = "CURLOPT_" . strtoupper($opt))) {
            curl_setopt($this->handle, constant($const), $args[0]);
        }
        # curl_action function
        elseif (function_exists($func = "curl_$opt")) {
            return call_user_func_array($func, array_merge(array($this->handle), $args));
        }
        # neither exists
        else {
            trigger_error("curl: unknown '$opt' option", E_USER_ERROR);
        }
        
        return $this;
    }
    
    
    /**
     * Append result-checks such as ["HTTP_CODE" => [200, 201]].
     *
     */
    function assert($list) {
        $this->assert += $list;
        return $this;
    }


    /**
     * Wrap exec() to test result handle for HTTP or CURL properties.
     *
     */
    function exec() {
        $args = func_get_args();
        $content = $this->__call("exec", $args);
        foreach ($this->assert as $option => $values) {
            if (!in_array($this->$option, $values)) {
                return NULL;
            }
        }
        return $content;
    }

    
    /**
     * retrieve constants 
     *
     */
    function __get($opt) {
        // @implicit error message from constant() for non-existant symbols
        return curl_getinfo($this->handle, constant($const = "CURLINFO_" . strtoupper($opt)));
    }
    
    
}




# hybrid constructor
function curl_multi($url=NULL) {
    return new curl_multi($url);
}


/**
 * Associative handler for multiple concurrent curl requests.
 * Not so fluent.
 *
 */
class curl_multi {


    /**
     * Indexed or associative list
     *
     */
    public $list = array();

    
    /**
     * Multi-handle
     *
     */
    public $mh = NULL;


    /**
     * Bind list of curl handles.
     *
     * An associative array can be passed here, either of prepared curl() handles,
     * just URLs, or an curl option array each.
     *
     */
    function __construct($list) {
    
        // optionally get indexed params as list
        if (func_num_args() >= 2) {
           $list = func_get_args();
        }
        
        // auto-wrap curl() handlers
        $list = array_map("curl", $list);

        // copy handle list
        $this->list = $list;
        
        // bind actual curl handles
        $this->mh = curl_multi_init();
        foreach ($list as $cw) {
            curl_multi_add_handle($this->mh, $cw->handle);
        }
    }
    
    
    /**
     * Run until all individual curl() handles are finished.
     *
     * Implicitly returns a list of result contents (associative array as well).
     *
     */
    function exec($timeout=1.50, $t=0.0, $running=1) {

        // process within timeframe
        while (($timeout > $t += $timeout/50) and $running) {

            curl_multi_exec($this->mh, $running);
            curl_multi_select($this->mh, $timeout/50);
        }

        // fetch results, then close handles
        return $this->close( $this->getcontent() );
    }

    
    /**
     * Retrieve results, associative array.
     *
     */
    function getcontent() {
        return array_map("curl_multi_getcontent", array_map("current",/*retrieve first property (->handle) each*/ $this->list));
    }
    

    /**
     * Close all curl() handles in $list, and the curl-multi wrapper.
     *
     */
    function close($r=NULL) {
        $this->__call("close", array());
        curl_multi_close($this->mh);
        return $r;
    }


    /**
     * Applies options on each curl handle.
     *
     */
    function __call($opt, $args) {
        foreach ($this->list as $cw) {
            $cw->__call($opt, $args);
        }
    }

}


?>