シリアライズ可能なcURLのラッパークラス

**Cookie** がシリアライズ可能なcURLのラッパークラスがあんまり無いような気がしたので作ってみました。

# Source

**BSD2条項ライセンス** でお願いします。

```php
<?php

/**
 * cURL
 * 
 * @author CertaiN
 * @github https://github.com/Certainist/cURL
 * @license BSD 2-Clause
 */
class cURL implements Serializable {
    
    /**
     * Default User-Agent.
     * 
     * @static
     * @access public
     */
    public static $defaultUserAgent = 'Chrome';
    
    private $ch;
    private $fp;
    private $userAgent;
    private $cookie;
    
    /**
     * You have to call parent::__construct() on your extended method.
     * 
     * @magic
     * @access public
     * @param string [$user_agent = null]
     */
    public function __construct($user_agent = null) {
        $list = static::getUserAgents();
        if (!func_num_args()) {
            $user_agent = static::$defaultUserAgent;
        }
        if (!is_string($user_agent)) {
            throw new InvalidArgumentException('User-Agent value type must be string.');
        }
        if (!is_array($list)) {
            throw new DomainException('static::getUserAgents() must return 1 dimentional assoc.');
        }
        if (!array_key_exists($user_agent, $list)) {
            throw new InvalidArgumentException('Unknown User-Agent.');
        }
        if (!is_string($list[$user_agent])) {
            throw new DomainException('static::getUserAgents() return array must contain string values.');
        }
        $this->userAgent = $list[$user_agent];
        $this->init();
    }
    
    /**
     * For GET requests.
     * 
     * @access public
     * @param string $url
     * @param mixed [&$info = null] Set result of curl_getinfo().
     * @return string Response body.
     */
    public function get($url, &$info = null) {
        if (!is_string($url)) {
            throw new InvalidArgumentException('URL value type must be string.');
        }
        if (!is_resource($this->ch)) {
            throw new BadMethodCallException('cURL resource is not initialized');
        }
        curl_setopt_array($this->ch, array(
            CURLOPT_URL => $url,
            CURLOPT_HTTPGET => true,
        ));
        return $this->exec($info);
    }
    
    /**
     * For POST requests.
     * 
     * @access public
     * @param string $url
     * @param mixed $params Query string or associative array.
     * @param mixed [&$info = null] Set result of curl_getinfo().
     * @return string Response body.
     */
    public function post($url, $params, &$info = null) {
        if (!is_string($url)) {
            throw new InvalidArgumentException('URL value type must be string.');
        }
        if (!is_resource($this->ch)) {
            throw new BadMethodCallException('cURL resource is not initialized');
        }
        curl_setopt_array($this->ch, array(
            CURLOPT_URL => $url,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $params,
        ));
        return $this->exec($info);
    }
    
    /**
     * Clear cookies.
     * 
     * @access public
     */
    public function clearCookies() {
        if (!is_resource($this->ch)) {
            throw new BadMethodCallException('cURL resource is not initialized');
        }
        ftruncate($this->fp, 0);
    }
    
    /**
     * Return the list of User-Agents.
     * You can extend this method.
     * 
     * @static
     * @access protected
     * @return array Associative array.
     */
    protected static function getUserAgents() {
        return array(
            'Chrome' =>
                'Mozilla/5.0 (Windows NT 6.1) ' .
                'AppleWebKit/537.36 (KHTML, like Gecko) ' .
                'Chrome/28.0.1500.63 Safari/537.36'
            ,
            'Firefox' =>
                'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:9.0.1) ' .
                'Gecko/20100101 Firefox/9.0.1'
            ,
            'Android' =>
                'Mozilla/5.0 (Linux; Android 4.1.1; Nexus 7 Build/JRO03S) ' .
                'AppleWebKit/535.19 (KHTML, like Gecko) ' .
                'Chrome/18.0.1025.166 Safari/535.19'
            ,
            'iOS' =>
                'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) ' .
                'AppleWebKit/536.26 (KHTML, like Gecko) ' .
                'Version/6.0 Mobile/10A403 Safari/8536.25'
            ,
            'Windows Phone' =>
                'Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; ' .
                'Trident/5.0; IEMobile/9.0; ' .
                'FujitsuToshibaMobileCommun; IS12T; KDDI)'
            ,
            'Internet Explorer' =>
                'Mozilla/5.0 (Windows NT 6.3; WOW64; ' . 
                'Trident/7.0; Touch; rv:11.0) like Gecko'
            ,
        );
    }
    
    /**
     * Serialize your own properties.
     * You can extend this method.
     * 
     * @access protected
     * @return mixed
     */
    protected function userSerialize() { return null; }
    
    /**
     * Unserialize your own properties.
     * You can extend this method.
     * 
     * @param anything $data
     * @access protected
     * @return mixed
     */
    protected function userUnserialize($data) { }
    
    /**
     * You have to call parent::__destruct() on your extended method.
     * 
     * @magic
     * @access public
     */
    public function __destruct() {
        if (is_resource($this->ch)) {
            curl_close($this->ch);
        }
        if (is_resource($this->fp)) {
            $this->cookie = stream_get_contents($this->fp);
            fclose($this->fp);
        }
    }
    
    final public function serialize() {
        $this->__destruct();
        $this->init($this->cookie);
        return serialize(array(
            $this->userAgent,
            $this->cookie,
            $this->userSerialize(),
        ));
    }
    
    final public function unserialize($data) {
        if (
            !$data = @unserialize($data) or
            !array_key_exists(0, $data) or
            !array_key_exists(1, $data) or
            !array_key_exists(2, $data) or
            !in_array($data[0], static::getUserAgents(), true) or
            !is_string($data[1])
        ) {
            throw new UnexpectedValueException('Invalid serial');
        }
        $this->userAgent = $data[0];
        $this->init($data[1]);
        $this->userUnserialize($data[2]);
    }
    
    private function exec(&$info) {
        $ret = curl_exec($this->ch);
        $info = curl_getinfo($this->ch);
        return $ret;
    }
    
    private function init($data = '') {
        $this->fp = tmpfile();
        if ($data !== '') {
            fwrite($this->fp, $data);
            rewind($this->fp);
        }
        $info = stream_get_meta_data($this->fp);
        $cookie_uri = $info['uri'];
        $this->ch = curl_init();
        curl_setopt_array($this->ch, array(
            CURLINFO_HEADER_OUT => true,
            CURLOPT_AUTOREFERER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT => 15,
            CURLOPT_MAXREDIRS => 5,
            CURLOPT_COOKIEFILE => $cookie_uri,
            CURLOPT_COOKIEJAR => $cookie_uri,
            CURLOPT_ENCODING => 'gzip, deflate',
            CURLOPT_USERAGENT => $this->userAgent,
            CURLOPT_HTTPHEADER => array(
                'Accept: ' .
                    'text/html,' . 
                    'application/xhtml+xml,' .
                    'application/xml' .
                    ';q=0.9,*/*;q=0.8'
                ,
                'Accept-Language: ' .
                    'ja,en-us;q=0.7,en;q=0.3'
                ,
            ),
        ));
    }
    
}
```

# Example

### ニコニコ動画にログインして履歴を取得する例

```php
class NicoNico extends cURL {
    
    public function __construct($mail_tel, $password) {
        parent::__construct('Chrome');
        $this->get('http://www.nicovideo.jp/login');
        $this->post(
            'https://secure.nicovideo.jp/secure/login?site=niconico',
            array(
                'next_url' => '',
                'mail_tel' => $mail_tel,
                'password' => $password,
            ),
            $info
        );
        if ($info['url'] !== 'http://www.nicovideo.jp/') {
            throw new RuntimeException('ログインに失敗しました');
        }
    }
    
    public function getHistory() {
        $regex = implode('.*?', array(
            '<div class="outer" id="outer_sm(\d*+)">',
            '<img src="([^"]*+)" alt="([^"]*+)" class="video" />',
            '<span class="videoTime">([^<]*+)</span>',
            '<p class="posttime">(\d*+年\d*+月\d*+日 \d*+:\d*+)',
            '<span>視聴回数(\d*+)回</span>',
            '<li class="play">再生:([^<]*+)</li>',
            '<li class="comment">コメント:([^<]*+)</li>',
            '<li class="mylist">マイリスト:<a href="[^"]*+">([^<]*+)</a></li>',
            '<li class="posttime">(\d*+年\d*+月\d*+日 \d*+:\d*+) 投稿</li>',
        ));
        $regex = "@{$regex}@s";
        $str = $this->get('http://www.nicovideo.jp/my/history');
        if (!preg_match_all($regex, $str, $matches, PREG_SET_ORDER)) {
            throw new RuntimeException('履歴取得に失敗しました');
        }
        foreach ($matches as $match) {
            $ret[] = array(
                'id' => $match[1],
                'url' => "http://www.nicovideo.jp/watch/sm{$match[1]}",
                'thumb_url' => $match[2],
                'title' => $match[3],
                'duraction' => $match[4],
                'watched_at' => $match[5],
                'watched_count' => $match[6],
                'meta_watched_count' => $match[7],
                'meta_comment_count' => $match[8],
                'meta_mylist_count' => $match[9],
                'meta_created_at' => "20{$match[10]}", 
            );
        }
        return $ret;
    }
    
}
```

```php
$nico = new NicoNico('aaaaa@bbbbb.cc.jp', 'xxxxxxx');
var_dump($nico->getHistory());
```

```text
array(30) {
  [0]=>
  array(11) {
    ["id"]=>
    string(8) "21280725"
    ["url"]=>
    string(40) "http://www.nicovideo.jp/watch/sm21280725"
    ["thumb_url"]=>
    string(45) "http://tn-skr2.smilevideo.jp/smile?i=21280725"
    ["title"]=>
    string(69) "【オツキミリサイタル】歌ってみた　＠ゆいこんぬ"
    ["duraction"]=>
    string(4) "3:45"
    ["watched_at"]=>
    string(23) "2013年12月05日 14:10"
    ["watched_count"]=>
    string(1) "1"
    ["meta_watched_count"]=>
    string(7) "316,137"
    ["meta_comment_count"]=>
    string(5) "4,322"
    ["meta_mylist_count"]=>
    string(6) "14,802"
    ["meta_created_at"]=>
    string(23) "2013年07月05日 17:57"
  }
  [1]=>
  ....
```

### このままシリアライズしてCookieを維持することも可能

```php
file_put_contents('NicoNico.dat', serialize($nico));
```

### 間違った継承のしかたをすると例外をスロー

- `parent::__construct()` をコールせずに使おうとすると **BadMethodCallException** をスロー
- `static::getUserAgents()` が正しい配列を返さないと **DomainException** をスロー