code:
<?php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
/**
 * PHP Version 5
 *
 * Copyright (c) 2001-2015, The PEAR developers
 *
 * This source file is subject to the BSD-2-Clause license,
 * that is bundled with this package in the file LICENSE, and is
 * available through the world-wide-web at the following url:
 * http://opensource.org/licenses/bsd-license.php.
 *
 * @category Console
 * @package  Console_Getopt
 * @author   Andrei Zmievski <andrei@php.net>
 * @license  http://opensource.org/licenses/bsd-license.php BSD-2-Clause
 * @version  CVS: $Id$
 * @link     http://pear.php.net/package/Console_Getopt
 */

require_once 'PEAR.php';

/**
 * Command-line options parsing class.
 *
 * @category Console
 * @package  Console_Getopt
 * @author   Andrei Zmievski <andrei@php.net>
 * @license  http://opensource.org/licenses/bsd-license.php BSD-2-Clause
 * @link     http://pear.php.net/package/Console_Getopt
 */
class Console_Getopt
{

    /**
     * Parses the command-line options.
     *
     * The first parameter to this function should be the list of command-line
     * arguments without the leading reference to the running program.
     *
     * The second parameter is a string of allowed short options. Each of the
     * option letters can be followed by a colon ':' to specify that the option
     * requires an argument, or a double colon '::' to specify that the option
     * takes an optional argument.
     *
     * The third argument is an optional array of allowed long options. The
     * leading '--' should not be included in the option name. Options that
     * require an argument should be followed by '=', and options that take an
     * option argument should be followed by '=='.
     *
     * The return value is an array of two elements: the list of parsed
     * options and the list of non-option command-line arguments. Each entry in
     * the list of parsed options is a pair of elements - the first one
     * specifies the option, and the second one specifies the option argument,
     * if there was one.
     *
     * Long and short options can be mixed.
     *
     * Most of the semantics of this function are based on GNU getopt_long().
     *
     * @param array  $args          an array of command-line arguments
     * @param string $short_options specifies the list of allowed short options
     * @param array  $long_options  specifies the list of allowed long options
     * @param boolean $skip_unknown suppresses Console_Getopt: unrecognized option
     *
     * @return array two-element array containing the list of parsed options and
     * the non-option arguments
     */
    public static function getopt2($args, $short_options, $long_options = null, $skip_unknown = false)
    {
        return Console_Getopt::doGetopt(2, $args, $short_options, $long_options, $skip_unknown);
    }

    /**
     * This function expects $args to start with the script name (POSIX-style).
     * Preserved for backwards compatibility.
     *
     * @param array  $args          an array of command-line arguments
     * @param string $short_options specifies the list of allowed short options
     * @param array  $long_options  specifies the list of allowed long options
     *
     * @see getopt2()
     * @return array two-element array containing the list of parsed options and
     * the non-option arguments
     */
    public static function getopt($args, $short_options, $long_options = null, $skip_unknown = false)
    {
        return Console_Getopt::doGetopt(1, $args, $short_options, $long_options, $skip_unknown);
    }

    /**
     * The actual implementation of the argument parsing code.
     *
     * @param int    $version       Version to use
     * @param array  $args          an array of command-line arguments
     * @param string $short_options specifies the list of allowed short options
     * @param array  $long_options  specifies the list of allowed long options
     * @param boolean $skip_unknown suppresses Console_Getopt: unrecognized option
     *
     * @return array
     */
    public static function doGetopt($version, $args, $short_options, $long_options = null, $skip_unknown = false)
    {
        // in case you pass directly readPHPArgv() as the first arg
        if (PEAR::isError($args)) {
            return $args;
        }

        if (empty($args)) {
            return array(array(), array());
        }

        $non_opts = $opts = array();

        settype($args, 'array');

        if ($long_options) {
            sort($long_options);
        }

        /*
         * Preserve backwards compatibility with callers that relied on
         * erroneous POSIX fix.
         */
        if ($version < 2) {
            if (isset($args[0]{0}) && $args[0]{0} != '-') {
                array_shift($args);
            }
        }

        for ($i = 0; $i < count($args); $i++) {
            $arg = $args[$i];
            /* The special element '--' means explicit end of
               options. Treat the rest of the arguments as non-options
               and end the loop. */
            if ($arg == '--') {
                $non_opts = array_merge($non_opts, array_slice($args, $i + 1));
                break;
            }

            if ($arg{0} != '-' || (strlen($arg) > 1 && $arg{1} == '-' && !$long_options)) {
                $non_opts = array_merge($non_opts, array_slice($args, $i));
                break;
            } elseif (strlen($arg) > 1 && $arg{1} == '-') {
                $error = Console_Getopt::_parseLongOption(substr($arg, 2),
                                                          $long_options,
                                                          $opts,
                                                          $i,
                                                          $args,
                                                          $skip_unknown);
                if (PEAR::isError($error)) {
                    return $error;
                }
            } elseif ($arg == '-') {
                // - is stdin
                $non_opts = array_merge($non_opts, array_slice($args, $i));
                break;
            } else {
                $error = Console_Getopt::_parseShortOption(substr($arg, 1),
                                                           $short_options,
                                                           $opts,
                                                           $i,
                                                           $args,
                                                           $skip_unknown);
                if (PEAR::isError($error)) {
                    return $error;
                }
            }
        }

        return array($opts, $non_opts);
    }

    /**
     * Parse short option
     *
     * @param string     $arg           Argument
     * @param string[]   $short_options Available short options
     * @param string[][] &$opts
     * @param int        &$argIdx
     * @param string[]   $args
     * @param boolean    $skip_unknown suppresses Console_Getopt: unrecognized option
     *
     * @return void
     */
    protected static function _parseShortOption($arg, $short_options, &$opts, &$argIdx, $args, $skip_unknown)
    {
        for ($i = 0; $i < strlen($arg); $i++) {
            $opt     = $arg{$i};
            $opt_arg = null;

            /* Try to find the short option in the specifier string. */
            if (($spec = strstr($short_options, $opt)) === false || $arg{$i} == ':') {
                if ($skip_unknown === true) {
                    break;
                }

                $msg = "Console_Getopt: unrecognized option -- $opt";
                return PEAR::raiseError($msg);
            }

            if (strlen($spec) > 1 && $spec{1} == ':') {
                if (strlen($spec) > 2 && $spec{2} == ':') {
                    if ($i + 1 < strlen($arg)) {
                        /* Option takes an optional argument. Use the remainder of
                           the arg string if there is anything left. */
                        $opts[] = array($opt, substr($arg, $i + 1));
                        break;
                    }
                } else {
                    /* Option requires an argument. Use the remainder of the arg
                       string if there is anything left. */
                    if ($i + 1 < strlen($arg)) {
                        $opts[] = array($opt,  substr($arg, $i + 1));
                        break;
                    } else if (isset($args[++$argIdx])) {
                        $opt_arg = $args[$argIdx];
                        /* Else use the next argument. */;
                        if (Console_Getopt::_isShortOpt($opt_arg)
                            || Console_Getopt::_isLongOpt($opt_arg)) {
                            $msg = "option requires an argument --$opt";
                            return PEAR::raiseError("Console_Getopt: " . $msg);
                        }
                    } else {
                        $msg = "option requires an argument --$opt";
                        return PEAR::raiseError("Console_Getopt: " . $msg);
                    }
                }
            }

            $opts[] = array($opt, $opt_arg);
        }
    }

    /**
     * Checks if an argument is a short option
     *
     * @param string $arg Argument to check
     *
     * @return bool
     */
    protected static function _isShortOpt($arg)
    {
        return strlen($arg) == 2 && $arg[0] == '-'
               && preg_match('/[a-zA-Z]/', $arg[1]);
    }

    /**
     * Checks if an argument is a long option
     *
     * @param string $arg Argument to check
     *
     * @return bool
     */
    protected static function _isLongOpt($arg)
    {
        return strlen($arg) > 2 && $arg[0] == '-' && $arg[1] == '-' &&
               preg_match('/[a-zA-Z]+$/', substr($arg, 2));
    }

    /**
     * Parse long option
     *
     * @param string     $arg          Argument
     * @param string[]   $long_options Available long options
     * @param string[][] &$opts
     * @param int        &$argIdx
     * @param string[]   $args
     *
     * @return void|PEAR_Error
     */
    protected static function _parseLongOption($arg, $long_options, &$opts, &$argIdx, $args, $skip_unknown)
    {
        @list($opt, $opt_arg) = explode('=', $arg, 2);

        $opt_len = strlen($opt);

        for ($i = 0; $i < count($long_options); $i++) {
            $long_opt  = $long_options[$i];
            $opt_start = substr($long_opt, 0, $opt_len);

            $long_opt_name = str_replace('=', '', $long_opt);

            /* Option doesn't match. Go on to the next one. */
            if ($long_opt_name != $opt) {
                continue;
            }

            $opt_rest = substr($long_opt, $opt_len);

            /* Check that the options uniquely matches one of the allowed
               options. */
            if ($i + 1 < count($long_options)) {
                $next_option_rest = substr($long_options[$i + 1], $opt_len);
            } else {
                $next_option_rest = '';
            }

            if ($opt_rest != '' && $opt{0} != '=' &&
                $i + 1 < count($long_options) &&
                $opt == substr($long_options[$i+1], 0, $opt_len) &&
                $next_option_rest != '' &&
                $next_option_rest{0} != '=') {

                $msg = "Console_Getopt: option --$opt is ambiguous";
                return PEAR::raiseError($msg);
            }

            if (substr($long_opt, -1) == '=') {
                if (substr($long_opt, -2) != '==') {
                    /* Long option requires an argument.
                       Take the next argument if one wasn't specified. */;
                    if (!strlen($opt_arg)) {
                        if (!isset($args[++$argIdx])) {
                            $msg = "Console_Getopt: option requires an argument --$opt";
                            return PEAR::raiseError($msg);
                        }
                        $opt_arg = $args[$argIdx];
                    }

                    if (Console_Getopt::_isShortOpt($opt_arg)
                        || Console_Getopt::_isLongOpt($opt_arg)) {
                        $msg = "Console_Getopt: option requires an argument --$opt";
                        return PEAR::raiseError($msg);
                    }
                }
            } else if ($opt_arg) {
                $msg = "Console_Getopt: option --$opt doesn't allow an argument";
                return PEAR::raiseError($msg);
            }

            $opts[] = array('--' . $opt, $opt_arg);
            return;
        }

        if ($skip_unknown === true) {
            return;
        }

        return PEAR::raiseError("Console_Getopt: unrecognized option --$opt");
    }

    /**
     * Safely read the $argv PHP array across different PHP configurations.
     * Will take care on register_globals and register_argc_argv ini directives
     *
     * @return mixed the $argv PHP array or PEAR error if not registered
     */
    public static function readPHPArgv()
    {
        global $argv;
        if (!is_array($argv)) {
            if (!@is_array($_SERVER['argv'])) {
                if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) {
                    $msg = "Could not read cmd args (register_argc_argv=Off?)";
                    return PEAR::raiseError("Console_Getopt: " . $msg);
                }
                return $GLOBALS['HTTP_SERVER_VARS']['argv'];
            }
            return $_SERVER['argv'];
        }
        return $argv;
    }

}


code:
*******************************************
Console_Getopt - Command-line option parser
*******************************************

This is a PHP implementation of "getopt" supporting both short and long options.
It helps parsing command line options in your PHP script.

Homepage: http://pear.php.net/package/Console_Getopt

.. image:: https://travis-ci.org/pear/Console_Getopt.svg?branch=master
    :target: https://travis-ci.org/pear/Console_Getopt

Alternatives
============

* Console_CommandLine__ (recommended)
* Console_GetoptPlus__

__ http://pear.php.net/package/Console_CommandLine
__ http://pear.php.net/package/Console_GetoptPlus

License
=======
BSD-2-Clause

repo_name: spookie/gnu

license: mit


Andrei Zmievski

role: Lead

Stig Bakken

role: Developer

Greg Beaver

role: Helper

description: More info available on: http://pear.php.net/package/Console_Getopt

type: library


code:
<?xml version="1.0" encoding="UTF-8"?>
<package packagerversion="1.9.2" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd">
 <name>Console_Getopt</name>
 <channel>pear.php.net</channel>
 <summary>Command-line option parser</summary>
 <description>This is a PHP implementation of "getopt" supporting both
short and long options.</description>
 <lead>
  <name>Andrei Zmievski</name>
  <user>andrei</user>
  <email>andrei@php.net</email>
  <active>no</active>
 </lead>
 <developer>
  <name>Stig Bakken</name>
  <user>ssb</user>
  <email>stig@php.net</email>
  <active>no</active>
 </developer>
 <helper>
  <name>Greg Beaver</name>
  <user>cellog</user>
  <email>cellog@php.net</email>
  <active>no</active>
 </helper>

 <date>2019-02-06</date>
 <version>
  <release>1.4.2</release>
  <api>1.4.0</api>
 </version>
 <stability>
  <release>stable</release>
  <api>stable</api>
 </stability>
 <license uri="http://opensource.org/licenses/bsd-license.php">BSD-2-Clause</license>

 <notes>
* Remove use of each(), which is removed in PHP 8
 </notes>

 <contents>
  <dir name="/">
   <dir name="Console">
    <file name="Getopt.php" role="php" />
   </dir>
   <dir name="tests">
    <file role="test" name="001-getopt.phpt" />
    <file role="test" name="bug10557.phpt" />
    <file role="test" name="bug11068.phpt" />
    <file role="test" name="bug13140.phpt" />
   </dir>
  </dir>
 </contents>

 <compatible>
  <name>PEAR</name>
  <channel>pear.php.net</channel>
  <min>1.4.0</min>
  <max>1.999.999</max>
 </compatible>

 <dependencies>
  <required>
   <php>
    <min>5.4.0</min>
   </php>
   <pearinstaller>
    <min>1.8.0</min>
   </pearinstaller>
  </required>
 </dependencies>

 <phprelease />

 <changelog>

  <release>
    <date>2019-02-06</date>
    <version>
     <release>1.4.2</release>
     <api>1.4.0</api>
    </version>
    <stability>
     <release>stable</release>
     <api>stable</api>
    </stability>
    <license uri="http://opensource.org/licenses/bsd-license.php">BSD-2-Clause</license>
    <notes>
    * Remove use of each(), which is removed in PHP 8
    </notes>
  </release>

  <release>
    <date>2015-07-20</date>
    <version>
     <release>1.4.1</release>
     <api>1.4.0</api>
    </version>
    <stability>
     <release>stable</release>
     <api>stable</api>
    </stability>
    <license uri="http://opensource.org/licenses/bsd-license.php">BSD-2-Clause</license>
    <notes>
     * Fix unit test on PHP 7 [cweiske]
    </notes>
  </release>

  <release>
    <date>2015-02-22</date>
    <version>
     <release>1.4.0</release>
     <api>1.4.0</api>
    </version>
    <stability>
     <release>stable</release>
     <api>stable</api>
    </stability>
    <license uri="http://opensource.org/licenses/bsd-license.php">BSD-2-Clause</license>
    <notes>
     * Change license to BSD-2-Clause
     * Set minimum PHP version to 5.4.0
     * Mark static methods with "static" keyword
    </notes>
  </release>

  <release>
    <date>2011-03-07</date>
    <version>
     <release>1.3.1</release>
     <api>1.3.0</api>
    </version>
    <stability>
     <release>stable</release>
     <api>stable</api>
    </stability>
    <license uri="http://www.php.net/license">PHP License</license>
    <notes>
     * Change the minimum PEAR installer dep to be lower
    </notes>
  </release>

  <release>
    <date>2010-12-11</date>
    <time>20:20:13</time>
    <version>
     <release>1.3.0</release>
     <api>1.3.0</api>
    </version>
    <stability>
     <release>stable</release>
     <api>stable</api>
    </stability>
    <license uri="http://www.php.net/license">PHP License</license>
    <notes>
   * Implement Request #13140: [PATCH] to skip unknown parameters. [patch by rquadling, improved on by dufuz]
    </notes>
  </release>

  <release>
   <date>2007-06-12</date>
   <version>
    <release>1.2.3</release>
    <api>1.2.1</api>
   </version>
   <stability>
    <release>stable</release>
    <api>stable</api>
   </stability>
   <license uri="http://www.php.net/license">PHP License</license>
   <notes>
* fix Bug #11068: No way to read plain "-" option [cardoe]
   </notes>
  </release>
  <release>
   <version>
    <release>1.2.2</release>
    <api>1.2.1</api>
   </version>
   <stability>
    <release>stable</release>
    <api>stable</api>
   </stability>
   <date>2007-02-17</date>
   <license uri="http://www.php.net/license">PHP License</license>
   <notes>
* fix Bug #4475: An ambiguous error occurred when specifying similar longoption name.
* fix Bug #10055: Not failing properly on short options missing required values
   </notes>
  </release>
  <release>
   <version>
    <release>1.2.1</release>
    <api>1.2.1</api>
   </version>
   <stability>
    <release>stable</release>
    <api>stable</api>
   </stability>
   <date>2006-12-08</date>
   <license uri="http://www.php.net/license">PHP License</license>
   <notes>
Fixed bugs #4448 (Long parameter values truncated with longoption parameter) and #7444 (Trailing spaces after php closing tag)
   </notes>
  </release>
  <release>
   <version>
    <release>1.2</release>
    <api>1.2</api>
   </version>
   <stability>
    <release>stable</release>
    <api>stable</api>
   </stability>
   <date>2003-12-11</date>
   <license uri="http://www.php.net/license">PHP License</license>
   <notes>
Fix to preserve BC with 1.0 and allow correct behaviour for new users
   </notes>
  </release>
  <release>
   <version>
    <release>1.0</release>
    <api>1.0</api>
   </version>
   <stability>
    <release>stable</release>
    <api>stable</api>
   </stability>
   <date>2002-09-13</date>
   <license uri="http://www.php.net/license">PHP License</license>
   <notes>
Stable release
   </notes>
  </release>
  <release>
   <version>
    <release>0.11</release>
    <api>0.11</api>
   </version>
   <stability>
    <release>beta</release>
    <api>beta</api>
   </stability>
   <date>2002-05-26</date>
   <license uri="http://www.php.net/license">PHP License</license>
   <notes>
POSIX getopt compatibility fix: treat first element of args
        array as command name
   </notes>
  </release>
  <release>
   <version>
    <release>0.10</release>
    <api>0.10</api>
   </version>
   <stability>
    <release>beta</release>
    <api>beta</api>
   </stability>
   <date>2002-05-12</date>
   <license uri="http://www.php.net/license">PHP License</license>
   <notes>
Packaging fix
   </notes>
  </release>
  <release>
   <version>
    <release>0.9</release>
    <api>0.9</api>
   </version>
   <stability>
    <release>beta</release>
    <api>beta</api>
   </stability>
   <date>2002-05-12</date>
   <license uri="http://www.php.net/license">PHP License</license>
   <notes>
Initial release
   </notes>
  </release>
 </changelog>
</package>


code:
--TEST--
Console_Getopt
--FILE--
<?php
require_once 'Console/Getopt.php';
PEAR::setErrorHandling(PEAR_ERROR_PRINT, "%s\n\n");

function test($argstr, $optstr) {
    $argv = preg_split('/[[:space:]]+/', $argstr);
    if (PEAR::isError($options = Console_Getopt::getopt($argv, $optstr))) {
        return;
    }
    $opts = $options[0];
    $non_opts = $options[1];
    $i = 0;
    print "options: ";
    foreach ($opts as $o => $d) {
        if ($i++ > 0) {
            print ", ";
        }
        print $d[0] . '=' . $d[1];
    }
    print "\n";
    print "params: " . implode(", ", $non_opts) . "\n";
    print "\n";
}

test("-abc", "abc");
test("-abc foo", "abc");
test("-abc foo", "abc:");
test("-abc foo bar gazonk", "abc");
test("-abc foo bar gazonk", "abc:");
test("-a -b -c", "abc");
test("-a -b -c", "abc:");
test("-abc", "ab:c");
test("-abc foo -bar gazonk", "abc");
?>
--EXPECT--
options: a=, b=, c=
params:

options: a=, b=, c=
params: foo

options: a=, b=, c=foo
params:

options: a=, b=, c=
params: foo, bar, gazonk

options: a=, b=, c=foo
params: bar, gazonk

options: a=, b=, c=
params:

Console_Getopt: option requires an argument --c

options: a=, b=c
params:

options: a=, b=, c=
params: foo, -bar, gazonk


code:
--TEST--
Console_Getopt [bug 10557]
--SKIPIF--
--FILE--
<?php
$_SERVER['argv'] =
$argv = array('hi', '-fjjohnston@mail.com', '--to', '--mailpack', '--debug');
require_once 'Console/Getopt.php';
$ret = Console_Getopt::getopt(Console_Getopt::readPHPArgv(), 'f:t:',
array('from=','to=','mailpack=','direction=','verbose','debug'));
if(PEAR::isError($ret))
{
	echo $ret->getMessage()."\n";
	echo 'FATAL';
	exit;
}

print_r($ret);
?>
--EXPECT--
Console_Getopt: option requires an argument --to
FATAL


code:
--TEST--
Console_Getopt [bug 11068]
--SKIPIF--
--FILE--
<?php
$_SERVER['argv'] =
$argv = array('hi', '-fjjohnston@mail.com', '--to', 'hi', '-');
require_once 'Console/Getopt.php';
$ret = Console_Getopt::getopt(Console_Getopt::readPHPArgv(), 'f:t:',
array('from=','to=','mailpack=','direction=','verbose','debug'));
if(PEAR::isError($ret))
{
	echo $ret->getMessage()."\n";
	echo 'FATAL';
	exit;
}

print_r($ret);
?>
--EXPECT--
Array
(
    [0] => Array
        (
            [0] => Array
                (
                    [0] => f
                    [1] => jjohnston@mail.com
                )

            [1] => Array
                (
                    [0] => --to
                    [1] => hi
                )

        )

    [1] => Array
        (
            [0] => -
        )

)


code:
--TEST--
Console_Getopt [bug 13140]
--SKIPIF--
--FILE--
<?php
$_SERVER['argv'] = $argv =
    array('--bob', '--foo' , '-bar', '--test', '-rq', 'thisshouldbehere');

require_once 'Console/Getopt.php';
$cg = new Console_GetOpt();

print_r($cg->getopt2($cg->readPHPArgv(), 't', array('test'), true));
print_r($cg->getopt2($cg->readPHPArgv(), 'bar', array('foo'), true));
?>
--EXPECT--
Array
(
    [0] => Array
        (
            [0] => Array
                (
                    [0] => --test
                    [1] =>
                )

        )

    [1] => Array
        (
            [0] => thisshouldbehere
        )

)
Array
(
    [0] => Array
        (
            [0] => Array
                (
                    [0] => --foo
                    [1] =>
                )

            [1] => Array
                (
                    [0] => b
                    [1] =>
                )

            [2] => Array
                (
                    [0] => a
                    [1] =>
                )

            [3] => Array
                (
                    [0] => r
                    [1] =>
                )

            [4] => Array
                (
                    [0] => r
                    [1] =>
                )

        )

    [1] => Array
        (
            [0] => thisshouldbehere
        )

)


code:
# Change Log

## 1.9.1 - 2019-02-02

### Added

- Updated type hints in doc blocks.

## 1.9.0 - 2019-01-03

### Added

- Support for PSR-18 clients
- Added traits `VersionBridgePlugin` and `VersionBridgeClient` to help plugins and clients to support both
  1.x and 2.x version of `php-http/client-common` and `php-http/httplug`.

### Changed

- [RetryPlugin] Renamed the configuration options for the exception retry callback from `decider` to `exception_decider`
  and `delay` to `exception_delay`. The old names still work but are deprecated.

## 1.8.2 - 2018-12-14

### Changed

- When multiple cookies exist, a single header with all cookies is sent as per RFC 6265 Section 5.4
- AddPathPlugin will now trim of ending slashes in paths

## 1.8.1 - 2018-10-09

### Fixed

- Reverted change to RetryPlugin so it again waits when retrying to avoid "can only throw objects" error.

## 1.8.0 - 2018-09-21

### Added

 - Add an option on ErrorPlugin to only throw exception on response with 5XX status code.

### Changed

- AddPathPlugin no longer add prefix multiple times if a request is restarted - it now only adds the prefix if that request chain has not yet passed through the AddPathPlugin
- RetryPlugin no longer wait for retried requests and use a deferred promise instead

### Fixed

- Decoder plugin will now remove header when there is no more encoding, instead of setting to an empty array

## 1.7.0 - 2017-11-30

### Added

- Symfony 4 support

### Changed

- Strict comparison in DecoderPlugin

## 1.6.0 - 2017-10-16

### Added

- Add HttpClientPool client to leverage load balancing and fallback mechanism [see the documentation](http://docs.php-http.org/en/latest/components/client-common.html) for more details.
- `PluginClientFactory` to create `PluginClient` instances.
- Added new option 'delay' for `RetryPlugin`.
- Added new option 'decider' for `RetryPlugin`.
- Supports more cookie date formats in the Cookie Plugin

### Changed

- The `RetryPlugin` does now wait between retries. To disable/change this feature you must write something like:

```php
$plugin = new RetryPlugin(['delay' => function(RequestInterface $request, Exception $e, $retries) {
  return 0;
});
```

### Deprecated

- The `debug_plugins` option for `PluginClient` is deprecated and will be removed in 2.0. Use the decorator design pattern instead like in [ProfilePlugin](https://github.com/php-http/HttplugBundle/blob/de33f9c14252f22093a5ec7d84f17535ab31a384/Collector/ProfilePlugin.php).

## 1.5.0 - 2017-03-30

### Added

- `QueryDefaultsPlugin` to add default query parameters.

## 1.4.2 - 2017-03-18

### Deprecated

- `DecoderPlugin` does not longer claim to support `compress` content encoding

### Fixed

- `CookiePlugin` allows main domain cookies to be sent/stored for subdomains
- `DecoderPlugin` uses the right `FilteredStream` to handle `deflate` content encoding

## 1.4.1 - 2017-02-20

### Fixed

- Cast return value of `StreamInterface::getSize` to string in `ContentLengthPlugin`

## 1.4.0 - 2016-11-04

### Added

- Add Path plugin
- Base URI plugin that combines Add Host and Add Path plugins

## 1.3.0 - 2016-10-16

### Changed

- Fix Emulated Trait to use Http based promise which respect the HttpAsyncClient interface
- Require Httplug 1.1 where we use HTTP specific promises.
- RedirectPlugin: use the full URL instead of the URI to properly keep track of redirects
- Add AddPathPlugin for API URLs with base path
- Add BaseUriPlugin that combines AddHostPlugin and AddPathPlugin

## 1.2.1 - 2016-07-26

### Changed

- AddHostPlugin also sets the port if specified

## 1.2.0 - 2016-07-14

### Added

- Suggest separate plugins in composer.json
- Introduced `debug_plugins` option for `PluginClient`

## 1.1.0 - 2016-05-04

### Added

- Add a flexible http client providing both contract, and only emulating what's necessary
- HTTP Client Router: route requests to underlying clients
- Plugin client and core plugins moved here from `php-http/plugins`

### Deprecated

- Extending client classes, they will be made final in version 2.0

## 1.0.0 - 2016-01-27

### Changed

- Remove useless interface in BatchException

## 0.2.0 - 2016-01-12

### Changed

- Updated package files
- Updated HTTPlug to RC1

## 0.1.1 - 2015-12-26

### Added

- Emulated clients

## 0.1.0 - 2015-12-25

### Added

- Batch client from utils
- Methods client from utils
- Emulators and decorators from client-tools


code:
# HTTP Client Common

[![Latest Version](https://img.shields.io/github/release/php-http/client-common.svg?style=flat-square)](https://github.com/php-http/client-common/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Build Status](https://img.shields.io/travis/php-http/client-common.svg?style=flat-square)](https://travis-ci.org/php-http/client-common)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/client-common.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/client-common)
[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/client-common.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/client-common)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/client-common.svg?style=flat-square)](https://packagist.org/packages/php-http/client-common)

**Common HTTP Client implementations and tools for HTTPlug.**

## Install

Via Composer

``` bash
$ composer require php-http/client-common
```

## Usage

This package provides common tools for HTTP Clients:

- BatchClient to handle sending requests in parallel
- A convenience client with HTTP method names as class methods
- Emulator, decorator layers for sync/async clients

## Documentation

Please see the [official documentation](http://docs.php-http.org/en/latest/components/client-common.html).

## Testing

``` bash
$ composer test
```

## Contributing

Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html).

## Security

If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org).

## License

The MIT License (MIT). Please see [License File](LICENSE) for more information.


php-http/client-common

description: Common HTTP Client implementations and tools for HTTPlug

license: MIT

keywords: http client httplug common

php-http/logger-plugin: PSR-3 Logger plugin

php-http/cache-plugin: PSR-6 Cache plugin

php-http/stopwatch-plugin: Symfony Stopwatch plugin

Http\Client\Common\: src/

test: vendor/bin/phpspec run

test-ci: vendor/bin/phpspec run -c phpspec.ci.yml


code:
<?php

namespace Http\Client\Common;

use Http\Client\Exception;
use Http\Client\HttpClient;
use Http\Client\Common\Exception\BatchException;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;

/**
 * BatchClient allow to sends multiple request and retrieve a Batch Result.
 *
 * This implementation simply loops over the requests and uses sendRequest with each of them.
 *
 * @author Joel Wurtz <jwurtz@jolicode.com>
 */
class BatchClient implements HttpClient
{
    /**
     * @var HttpClient|ClientInterface
     */
    private $client;

    /**
     * @param HttpClient|ClientInterface  $client
     */
    public function __construct($client)
    {
        if (!($client instanceof HttpClient) && !($client instanceof ClientInterface)) {
            throw new \LogicException('Client must be an instance of Http\\Client\\HttpClient or Psr\\Http\\Client\\ClientInterface');
        }

        $this->client = $client;
    }

    /**
     * {@inheritdoc}
     */
    public function sendRequest(RequestInterface $request)
    {
        return $this->client->sendRequest($request);
    }

    /**
     * Send several requests.
     *
     * You may not assume that the requests are executed in a particular order. If the order matters
     * for your application, use sendRequest sequentially.
     *
     * @param RequestInterface[] The requests to send
     *
     * @return BatchResult Containing one result per request
     *
     * @throws BatchException If one or more requests fails. The exception gives access to the
     *                        BatchResult with a map of request to result for success, request to
     *                        exception for failures
     */
    public function sendRequests(array $requests)
    {
        $batchResult = new BatchResult();

        foreach ($requests as $request) {
            try {
                $response = $this->sendRequest($request);
                $batchResult = $batchResult->addResponse($request, $response);
            } catch (Exception $e) {
                $batchResult = $batchResult->addException($request, $e);
            }
        }

        if ($batchResult->hasExceptions()) {
            throw new BatchException($batchResult);
        }

        return $batchResult;
    }
}


code:
<?php

namespace Http\Client\Common;

use Http\Client\Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
 * Responses and exceptions returned from parallel request execution.
 *
 * @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
 */
final class BatchResult
{
    /**
     * @var \SplObjectStorage
     */
    private $responses;

    /**
     * @var \SplObjectStorage
     */
    private $exceptions;

    public function __construct()
    {
        $this->responses = new \SplObjectStorage();
        $this->exceptions = new \SplObjectStorage();
    }

    /**
     * Checks if there are any successful responses at all.
     *
     * @return bool
     */
    public function hasResponses()
    {
        return $this->responses->count() > 0;
    }

    /**
     * Returns all successful responses.
     *
     * @return ResponseInterface[]
     */
    public function getResponses()
    {
        $responses = [];

        foreach ($this->responses as $request) {
            $responses[] = $this->responses[$request];
        }

        return $responses;
    }

    /**
     * Checks if there is a successful response for a request.
     *
     * @param RequestInterface $request
     *
     * @return bool
     */
    public function isSuccessful(RequestInterface $request)
    {
        return $this->responses->contains($request);
    }

    /**
     * Returns the response for a successful request.
     *
     * @param RequestInterface $request
     *
     * @return ResponseInterface
     *
     * @throws \UnexpectedValueException If request was not part of the batch or failed
     */
    public function getResponseFor(RequestInterface $request)
    {
        try {
            return $this->responses[$request];
        } catch (\UnexpectedValueException $e) {
            throw new \UnexpectedValueException('Request not found', $e->getCode(), $e);
        }
    }

    /**
     * Adds a response in an immutable way.
     *
     * @param RequestInterface  $request
     * @param ResponseInterface $response
     *
     * @return BatchResult the new BatchResult with this request-response pair added to it
     */
    public function addResponse(RequestInterface $request, ResponseInterface $response)
    {
        $new = clone $this;
        $new->responses->attach($request, $response);

        return $new;
    }

    /**
     * Checks if there are any unsuccessful requests at all.
     *
     * @return bool
     */
    public function hasExceptions()
    {
        return $this->exceptions->count() > 0;
    }

    /**
     * Returns all exceptions for the unsuccessful requests.
     *
     * @return Exception[]
     */
    public function getExceptions()
    {
        $exceptions = [];

        foreach ($this->exceptions as $request) {
            $exceptions[] = $this->exceptions[$request];
        }

        return $exceptions;
    }

    /**
     * Checks if there is an exception for a request, meaning the request failed.
     *
     * @param RequestInterface $request
     *
     * @return bool
     */
    public function isFailed(RequestInterface $request)
    {
        return $this->exceptions->contains($request);
    }

    /**
     * Returns the exception for a failed request.
     *
     * @param RequestInterface $request
     *
     * @return Exception
     *
     * @throws \UnexpectedValueException If request was not part of the batch or was successful
     */
    public function getExceptionFor(RequestInterface $request)
    {
        try {
            return $this->exceptions[$request];
        } catch (\UnexpectedValueException $e) {
            throw new \UnexpectedValueException('Request not found', $e->getCode(), $e);
        }
    }

    /**
     * Adds an exception in an immutable way.
     *
     * @param RequestInterface $request
     * @param Exception        $exception
     *
     * @return BatchResult the new BatchResult with this request-exception pair added to it
     */
    public function addException(RequestInterface $request, Exception $exception)
    {
        $new = clone $this;
        $new->exceptions->attach($request, $exception);

        return $new;
    }

    public function __clone()
    {
        $this->responses = clone $this->responses;
        $this->exceptions = clone $this->exceptions;
    }
}


code:
<?php

namespace Http\Client\Common;

use Http\Client\Exception;
use Http\Promise\Promise;
use Psr\Http\Message\ResponseInterface;

/**
 * A deferred allow to return a promise which has not been resolved yet.
 */
class Deferred implements Promise
{
    private $value;

    private $failure;

    private $state;

    private $waitCallback;

    private $onFulfilledCallbacks;

    private $onRejectedCallbacks;

    public function __construct(callable $waitCallback)
    {
        $this->waitCallback = $waitCallback;
        $this->state = Promise::PENDING;
        $this->onFulfilledCallbacks = [];
        $this->onRejectedCallbacks = [];
    }

    /**
     * {@inheritdoc}
     */
    public function then(callable $onFulfilled = null, callable $onRejected = null)
    {
        $deferred = new self($this->waitCallback);

        $this->onFulfilledCallbacks[] = function (ResponseInterface $response) use ($onFulfilled, $deferred) {
            try {
                if (null !== $onFulfilled) {
                    $response = $onFulfilled($response);
                }
                $deferred->resolve($response);
            } catch (Exception $exception) {
                $deferred->reject($exception);
            }
        };

        $this->onRejectedCallbacks[] = function (Exception $exception) use ($onRejected, $deferred) {
            try {
                if (null !== $onRejected) {
                    $response = $onRejected($exception);
                    $deferred->resolve($response);

                    return;
                }
                $deferred->reject($exception);
            } catch (Exception $newException) {
                $deferred->reject($newException);
            }
        };

        return $deferred;
    }

    /**
     * {@inheritdoc}
     */
    public function getState()
    {
        return $this->state;
    }

    /**
     * Resolve this deferred with a Response.
     */
    public function resolve(ResponseInterface $response)
    {
        if (self::PENDING !== $this->state) {
            return;
        }

        $this->value = $response;
        $this->state = self::FULFILLED;

        foreach ($this->onFulfilledCallbacks as $onFulfilledCallback) {
            $onFulfilledCallback($response);
        }
    }

    /**
     * Reject this deferred with an Exception.
     */
    public function reject(Exception $exception)
    {
        if (self::PENDING !== $this->state) {
            return;
        }

        $this->failure = $exception;
        $this->state = self::REJECTED;

        foreach ($this->onRejectedCallbacks as $onRejectedCallback) {
            $onRejectedCallback($exception);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function wait($unwrap = true)
    {
        if (self::PENDING === $this->state) {
            $callback = $this->waitCallback;
            $callback();
        }

        if (!$unwrap) {
            return;
        }

        if (self::FULFILLED === $this->state) {
            return $this->value;
        }

        throw $this->failure;
    }
}


code:
<?php

namespace Http\Client\Common;

use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;

/**
 * Emulates an async HTTP client.
 *
 * This should be replaced by an anonymous class in PHP 7.
 *
 * @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
 */
class EmulatedHttpAsyncClient implements HttpClient, HttpAsyncClient
{
    use HttpAsyncClientEmulator;
    use HttpClientDecorator;

    /**
     * @param HttpClient|ClientInterface $httpClient
     */
    public function __construct($httpClient)
    {
        if (!($httpClient instanceof HttpClient) && !($httpClient instanceof ClientInterface)) {
            throw new \LogicException('Client must be an instance of Http\\Client\\HttpClient or Psr\\Http\\Client\\ClientInterface');
        }

        $this->httpClient = $httpClient;
    }
}


code:
<?php

namespace Http\Client\Common;

use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;

/**
 * Emulates an HTTP client.
 *
 * This should be replaced by an anonymous class in PHP 7.
 *
 * @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
 */
class EmulatedHttpClient implements HttpClient, HttpAsyncClient
{
    use HttpAsyncClientDecorator;
    use HttpClientEmulator;

    /**
     * @param HttpAsyncClient $httpAsyncClient
     */
    public function __construct(HttpAsyncClient $httpAsyncClient)
    {
        $this->httpAsyncClient = $httpAsyncClient;
    }
}


code:
<?php

namespace Http\Client\Common\Exception;

use Http\Client\Exception\TransferException;
use Http\Client\Common\BatchResult;

/**
 * This exception is thrown when HttpClient::sendRequests led to at least one failure.
 *
 * It gives access to a BatchResult with the request-exception and request-response pairs.
 *
 * @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
 */
final class BatchException extends TransferException
{
    /**
     * @var BatchResult
     */
    private $result;

    /**
     * @param BatchResult $result
     */
    public function __construct(BatchResult $result)
    {
        $this->result = $result;
    }

    /**
     * Returns the BatchResult that contains all responses and exceptions.
     *
     * @return BatchResult
     */
    public function getResult()
    {
        return $this->result;
    }
}


code:
<?php

namespace Http\Client\Common\Exception;

use Http\Client\Exception\HttpException;

/**
 * Thrown when circular redirection is detected.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
class CircularRedirectionException extends HttpException
{
}


code:
<?php

namespace Http\Client\Common\Exception;

use Http\Client\Exception\HttpException;

/**
 * Thrown when there is a client error (4xx).
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
class ClientErrorException extends HttpException
{
}


code:
<?php

namespace Http\Client\Common\Exception;

use Http\Client\Exception\TransferException;

/**
 * Thrown when a http client cannot be chosen in a pool.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
class HttpClientNotFoundException extends TransferException
{
}


code:
<?php

namespace Http\Client\Common\Exception;

use Http\Client\Exception\RequestException;

/**
 * Thrown when the Plugin Client detects an endless loop.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
class LoopException extends RequestException
{
}


code:
<?php

namespace Http\Client\Common\Exception;

use Http\Client\Exception\HttpException;

/**
 * Redirect location cannot be chosen.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
class MultipleRedirectionException extends HttpException
{
}


code:
<?php

namespace Http\Client\Common\Exception;

use Http\Client\Exception\HttpException;

/**
 * Thrown when there is a server error (5xx).
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
class ServerErrorException extends HttpException
{
}


code:
<?php

namespace Http\Client\Common;

use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;

/**
 * A flexible http client, which implements both interface and will emulate
 * one contract, the other, or none at all depending on the injected client contract.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
final class FlexibleHttpClient implements HttpClient, HttpAsyncClient
{
    use HttpClientDecorator;
    use HttpAsyncClientDecorator;

    /**
     * @param HttpClient|HttpAsyncClient|ClientInterface $client
     */
    public function __construct($client)
    {
        if (!($client instanceof HttpClient) && !($client instanceof HttpAsyncClient) && !($client instanceof ClientInterface)) {
            throw new \LogicException('Client must be an instance of Http\\Client\\HttpClient or Http\\Client\\HttpAsyncClient');
        }

        $this->httpClient = $client;
        $this->httpAsyncClient = $client;

        if (!($this->httpClient instanceof HttpClient) && !($client instanceof ClientInterface)) {
            $this->httpClient = new EmulatedHttpClient($this->httpClient);
        }

        if (!($this->httpAsyncClient instanceof HttpAsyncClient)) {
            $this->httpAsyncClient = new EmulatedHttpAsyncClient($this->httpAsyncClient);
        }
    }
}


code:
<?php

namespace Http\Client\Common;

use Http\Client\HttpAsyncClient;
use Psr\Http\Message\RequestInterface;

/**
 * Decorates an HTTP Async Client.
 *
 * @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
 */
trait HttpAsyncClientDecorator
{
    /**
     * @var HttpAsyncClient
     */
    protected $httpAsyncClient;

    /**
     * {@inheritdoc}
     *
     * @see HttpAsyncClient::sendAsyncRequest
     */
    public function sendAsyncRequest(RequestInterface $request)
    {
        return $this->httpAsyncClient->sendAsyncRequest($request);
    }
}


code:
<?php

namespace Http\Client\Common;

use Http\Client\Exception;
use Http\Client\Promise;
use Psr\Http\Message\RequestInterface;

/**
 * Emulates an HTTP Async Client in an HTTP Client.
 *
 * @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
 */
trait HttpAsyncClientEmulator
{
    /**
     * {@inheritdoc}
     *
     * @see HttpClient::sendRequest
     */
    abstract public function sendRequest(RequestInterface $request);

    /**
     * {@inheritdoc}
     *
     * @see HttpAsyncClient::sendAsyncRequest
     */
    public function sendAsyncRequest(RequestInterface $request)
    {
        try {
            return new Promise\HttpFulfilledPromise($this->sendRequest($request));
        } catch (Exception $e) {
            return new Promise\HttpRejectedPromise($e);
        }
    }
}


code:
<?php

namespace Http\Client\Common;

use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;

/**
 * Decorates an HTTP Client.
 *
 * @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
 */
trait HttpClientDecorator
{
    /**
     * @var HttpClient|ClientInterface
     */
    protected $httpClient;

    /**
     * {@inheritdoc}
     *
     * @see HttpClient::sendRequest
     */
    public function sendRequest(RequestInterface $request)
    {
        return $this->httpClient->sendRequest($request);
    }
}


code:
<?php

namespace Http\Client\Common;

use Psr\Http\Message\RequestInterface;

/**
 * Emulates an HTTP Client in an HTTP Async Client.
 *
 * @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
 */
trait HttpClientEmulator
{
    /**
     * {@inheritdoc}
     *
     * @see HttpClient::sendRequest
     */
    public function sendRequest(RequestInterface $request)
    {
        $promise = $this->sendAsyncRequest($request);

        return $promise->wait();
    }

    /**
     * {@inheritdoc}
     *
     * @see HttpAsyncClient::sendAsyncRequest
     */
    abstract public function sendAsyncRequest(RequestInterface $request);
}


code:
<?php

namespace Http\Client\Common;

use Http\Client\Common\Exception\HttpClientNotFoundException;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;

/**
 * A http client pool allows to send requests on a pool of different http client using a specific strategy (least used,
 * round robin, ...).
 */
abstract class HttpClientPool implements HttpAsyncClient, HttpClient
{
    /**
     * @var HttpClientPoolItem[]
     */
    protected $clientPool = [];

    /**
     * Add a client to the pool.
     *
     * @param HttpClient|HttpAsyncClient|HttpClientPoolItem|ClientInterface $client
     */
    public function addHttpClient($client)
    {
        if (!$client instanceof HttpClientPoolItem) {
            $client = new HttpClientPoolItem($client);
        }

        $this->clientPool[] = $client;
    }

    /**
     * Return an http client given a specific strategy.
     *
     * @throws HttpClientNotFoundException When no http client has been found into the pool
     *
     * @return HttpClientPoolItem Return a http client that can do both sync or async
     */
    abstract protected function chooseHttpClient();

    /**
     * {@inheritdoc}
     */
    public function sendAsyncRequest(RequestInterface $request)
    {
        return $this->chooseHttpClient()->sendAsyncRequest($request);
    }

    /**
     * {@inheritdoc}
     */
    public function sendRequest(RequestInterface $request)
    {
        return $this->chooseHttpClient()->sendRequest($request);
    }
}


code:
<?php

namespace Http\Client\Common\HttpClientPool;

use Http\Client\Common\Exception\HttpClientNotFoundException;
use Http\Client\Common\HttpClientPool;
use Http\Client\Common\HttpClientPoolItem;

/**
 * LeastUsedClientPool will choose the client with the less current request in the pool.
 *
 * This strategy is only useful when doing async request
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
final class LeastUsedClientPool extends HttpClientPool
{
    /**
     * {@inheritdoc}
     */
    protected function chooseHttpClient()
    {
        $clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) {
            return !$clientPoolItem->isDisabled();
        });

        if (0 === count($clientPool)) {
            throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool');
        }

        usort($clientPool, function (HttpClientPoolItem $clientA, HttpClientPoolItem $clientB) {
            if ($clientA->getSendingRequestCount() === $clientB->getSendingRequestCount()) {
                return 0;
            }

            if ($clientA->getSendingRequestCount() < $clientB->getSendingRequestCount()) {
                return -1;
            }

            return 1;
        });

        return reset($clientPool);
    }
}


code:
<?php

namespace Http\Client\Common\HttpClientPool;

use Http\Client\Common\Exception\HttpClientNotFoundException;
use Http\Client\Common\HttpClientPool;
use Http\Client\Common\HttpClientPoolItem;

/**
 * RoundRobinClientPool will choose the next client in the pool.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
final class RandomClientPool extends HttpClientPool
{
    /**
     * {@inheritdoc}
     */
    protected function chooseHttpClient()
    {
        $clientPool = array_filter($this->clientPool, function (HttpClientPoolItem $clientPoolItem) {
            return !$clientPoolItem->isDisabled();
        });

        if (0 === count($clientPool)) {
            throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool');
        }

        return $clientPool[array_rand($clientPool)];
    }
}


code:
<?php

namespace Http\Client\Common\HttpClientPool;

use Http\Client\Common\Exception\HttpClientNotFoundException;
use Http\Client\Common\HttpClientPool;

/**
 * RoundRobinClientPool will choose the next client in the pool.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
final class RoundRobinClientPool extends HttpClientPool
{
    /**
     * {@inheritdoc}
     */
    protected function chooseHttpClient()
    {
        $last = current($this->clientPool);

        do {
            $client = next($this->clientPool);

            if (false === $client) {
                $client = reset($this->clientPool);

                if (false === $client) {
                    throw new HttpClientNotFoundException('Cannot choose a http client as there is no one present in the pool');
                }
            }

            // Case when there is only one and the last one has been disabled
            if ($last === $client && $client->isDisabled()) {
                throw new HttpClientNotFoundException('Cannot choose a http client as there is no one enabled in the pool');
            }
        } while ($client->isDisabled());

        return $client;
    }
}


code:
<?php

namespace Http\Client\Common;

use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Http\Client\Exception;

/**
 * A HttpClientPoolItem represent a HttpClient inside a Pool.
 *
 * It is disabled when a request failed and can be reenable after a certain number of seconds
 * It also keep tracks of the current number of request the client is currently sending (only usable for async method)
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
class HttpClientPoolItem implements HttpClient, HttpAsyncClient
{
    /**
     * @var int Number of request this client is currently sending
     */
    private $sendingRequestCount = 0;

    /**
     * @var \DateTime|null Time when this client has been disabled or null if enable
     */
    private $disabledAt;

    /**
     * @var int|null Number of seconds after this client is reenable, by default null: never reenable this client
     */
    private $reenableAfter;

    /**
     * @var FlexibleHttpClient A http client responding to async and sync request
     */
    private $client;

    /**
     * @param HttpClient|HttpAsyncClient|ClientInterface $client
     * @param null|int                   $reenableAfter Number of seconds after this client is reenable
     */
    public function __construct($client, $reenableAfter = null)
    {
        $this->client = new FlexibleHttpClient($client);
        $this->reenableAfter = $reenableAfter;
    }

    /**
     * {@inheritdoc}
     */
    public function sendRequest(RequestInterface $request)
    {
        if ($this->isDisabled()) {
            throw new Exception\RequestException('Cannot send the request as this client has been disabled', $request);
        }

        try {
            $this->incrementRequestCount();
            $response = $this->client->sendRequest($request);
            $this->decrementRequestCount();
        } catch (Exception $e) {
            $this->disable();
            $this->decrementRequestCount();

            throw $e;
        }

        return $response;
    }

    /**
     * {@inheritdoc}
     */
    public function sendAsyncRequest(RequestInterface $request)
    {
        if ($this->isDisabled()) {
            throw new Exception\RequestException('Cannot send the request as this client has been disabled', $request);
        }

        $this->incrementRequestCount();

        return $this->client->sendAsyncRequest($request)->then(function ($response) {
            $this->decrementRequestCount();

            return $response;
        }, function ($exception) {
            $this->disable();
            $this->decrementRequestCount();

            throw $exception;
        });
    }

    /**
     * Whether this client is disabled or not.
     *
     * Will also reactivate this client if possible
     *
     * @internal
     *
     * @return bool
     */
    public function isDisabled()
    {
        $disabledAt = $this->getDisabledAt();

        if (null !== $this->reenableAfter && null !== $disabledAt) {
            // Reenable after a certain time
            $now = new \DateTime();

            if (($now->getTimestamp() - $disabledAt->getTimestamp()) >= $this->reenableAfter) {
                $this->enable();

                return false;
            }

            return true;
        }

        return null !== $disabledAt;
    }

    /**
     * Get current number of request that is send by the underlying http client.
     *
     * @internal
     *
     * @return int
     */
    public function getSendingRequestCount()
    {
        return $this->sendingRequestCount;
    }

    /**
     * Return when this client has been disabled or null if it's enabled.
     *
     * @return \DateTime|null
     */
    private function getDisabledAt()
    {
        return $this->disabledAt;
    }

    /**
     * Increment the request count.
     */
    private function incrementRequestCount()
    {
        ++$this->sendingRequestCount;
    }

    /**
     * Decrement the request count.
     */
    private function decrementRequestCount()
    {
        --$this->sendingRequestCount;
    }

    /**
     * Enable the current client.
     */
    private function enable()
    {
        $this->disabledAt = null;
    }

    /**
     * Disable the current client.
     */
    private function disable()
    {
        $this->disabledAt = new \DateTime('now');
    }
}


code:
<?php

namespace Http\Client\Common;

use Http\Client\Exception\RequestException;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Http\Message\RequestMatcher;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;

/**
 * Route a request to a specific client in the stack based using a RequestMatcher.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
final class HttpClientRouter implements HttpClient, HttpAsyncClient
{
    /**
     * @var array
     */
    private $clients = [];

    /**
     * {@inheritdoc}
     */
    public function sendRequest(RequestInterface $request)
    {
        $client = $this->chooseHttpClient($request);

        return $client->sendRequest($request);
    }

    /**
     * {@inheritdoc}
     */
    public function sendAsyncRequest(RequestInterface $request)
    {
        $client = $this->chooseHttpClient($request);

        return $client->sendAsyncRequest($request);
    }

    /**
     * Add a client to the router.
     *
     * @param HttpClient|HttpAsyncClient|ClientInterface $client
     * @param RequestMatcher             $requestMatcher
     */
    public function addClient($client, RequestMatcher $requestMatcher)
    {
        $this->clients[] = [
            'matcher' => $requestMatcher,
            'client' => new FlexibleHttpClient($client),
        ];
    }

    /**
     * Choose an HTTP client given a specific request.
     *
     * @param RequestInterface $request
     *
     * @return HttpClient|HttpAsyncClient|ClientInterface
     */
    protected function chooseHttpClient(RequestInterface $request)
    {
        foreach ($this->clients as $client) {
            if ($client['matcher']->matches($request)) {
                return $client['client'];
            }
        }

        throw new RequestException('No client found for the specified request', $request);
    }
}


code:
<?php

namespace Http\Client\Common;

use Http\Client\Exception;
use Http\Client\HttpClient;
use Http\Message\RequestFactory;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;

/**
 * Convenience HTTP client that integrates the MessageFactory in order to send
 * requests in the following form:.
 *
 * $client
 *     ->get('/foo')
 *     ->post('/bar')
 * ;
 *
 * The client also exposes the sendRequest methods of the wrapped HttpClient.
 *
 * @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
 * @author David Buchmann <mail@davidbu.ch>
 */
class HttpMethodsClient implements HttpClient
{
    /**
     * @var HttpClient|ClientInterface
     */
    private $httpClient;

    /**
     * @var RequestFactory
     */
    private $requestFactory;

    /**
     * @param HttpClient|ClientInterface $httpClient     The client to send requests with
     * @param RequestFactory             $requestFactory The message factory to create requests
     */
    public function __construct($httpClient, RequestFactory $requestFactory)
    {
        if (!($httpClient instanceof HttpClient) && !($httpClient instanceof ClientInterface)) {
            throw new \LogicException('Client must be an instance of Http\\Client\\HttpClient or Psr\\Http\\Client\\ClientInterface');
        }

        $this->httpClient = $httpClient;
        $this->requestFactory = $requestFactory;
    }

    /**
     * Sends a GET request.
     *
     * @param string|UriInterface $uri
     * @param array               $headers
     *
     * @throws Exception
     *
     * @return ResponseInterface
     */
    public function get($uri, array $headers = [])
    {
        return $this->send('GET', $uri, $headers, null);
    }

    /**
     * Sends an HEAD request.
     *
     * @param string|UriInterface $uri
     * @param array               $headers
     *
     * @throws Exception
     *
     * @return ResponseInterface
     */
    public function head($uri, array $headers = [])
    {
        return $this->send('HEAD', $uri, $headers, null);
    }

    /**
     * Sends a TRACE request.
     *
     * @param string|UriInterface $uri
     * @param array               $headers
     *
     * @throws Exception
     *
     * @return ResponseInterface
     */
    public function trace($uri, array $headers = [])
    {
        return $this->send('TRACE', $uri, $headers, null);
    }

    /**
     * Sends a POST request.
     *
     * @param string|UriInterface         $uri
     * @param array                       $headers
     * @param string|StreamInterface|null $body
     *
     * @throws Exception
     *
     * @return ResponseInterface
     */
    public function post($uri, array $headers = [], $body = null)
    {
        return $this->send('POST', $uri, $headers, $body);
    }

    /**
     * Sends a PUT request.
     *
     * @param string|UriInterface         $uri
     * @param array                       $headers
     * @param string|StreamInterface|null $body
     *
     * @throws Exception
     *
     * @return ResponseInterface
     */
    public function put($uri, array $headers = [], $body = null)
    {
        return $this->send('PUT', $uri, $headers, $body);
    }

    /**
     * Sends a PATCH request.
     *
     * @param string|UriInterface         $uri
     * @param array                       $headers
     * @param string|StreamInterface|null $body
     *
     * @throws Exception
     *
     * @return ResponseInterface
     */
    public function patch($uri, array $headers = [], $body = null)
    {
        return $this->send('PATCH', $uri, $headers, $body);
    }

    /**
     * Sends a DELETE request.
     *
     * @param string|UriInterface         $uri
     * @param array                       $headers
     * @param string|StreamInterface|null $body
     *
     * @throws Exception
     *
     * @return ResponseInterface
     */
    public function delete($uri, array $headers = [], $body = null)
    {
        return $this->send('DELETE', $uri, $headers, $body);
    }

    /**
     * Sends an OPTIONS request.
     *
     * @param string|UriInterface         $uri
     * @param array                       $headers
     * @param string|StreamInterface|null $body
     *
     * @throws Exception
     *
     * @return ResponseInterface
     */
    public function options($uri, array $headers = [], $body = null)
    {
        return $this->send('OPTIONS', $uri, $headers, $body);
    }

    /**
     * Sends a request with any HTTP method.
     *
     * @param string                      $method  HTTP method to use
     * @param string|UriInterface         $uri
     * @param array                       $headers
     * @param string|StreamInterface|null $body
     *
     * @throws Exception
     *
     * @return ResponseInterface
     */
    public function send($method, $uri, array $headers = [], $body = null)
    {
        return $this->sendRequest($this->requestFactory->createRequest(
            $method,
            $uri,
            $headers,
            $body
        ));
    }

    /**
     * Forward to the underlying HttpClient.
     *
     * {@inheritdoc}
     */
    public function sendRequest(RequestInterface $request)
    {
        return $this->httpClient->sendRequest($request);
    }
}


code:
<?php

namespace Http\Client\Common;

use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;

/**
 * A plugin is a middleware to transform the request and/or the response.
 *
 * The plugin can:
 *  - break the chain and return a response
 *  - dispatch the request to the next middleware
 *  - restart the request
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
interface Plugin
{
    /**
     * Handle the request and return the response coming from the next callable.
     *
     * @see http://docs.php-http.org/en/latest/plugins/build-your-own.html
     *
     * @param RequestInterface $request
     * @param callable         $next    Next middleware in the chain, the request is passed as the first argument
     * @param callable         $first   First middleware in the chain, used to to restart a request
     *
     * @return Promise Resolves a PSR-7 Response or fails with an Http\Client\Exception (The same as HttpAsyncClient).
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first);
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Add schema, host and port to a request. Can be set to overwrite the schema and host if desired.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
final class AddHostPlugin implements Plugin
{
    /**
     * @var UriInterface
     */
    private $host;

    /**
     * @var bool
     */
    private $replace;

    /**
     * @param UriInterface $host
     * @param array        $config {
     *
     *     @var bool $replace True will replace all hosts, false will only add host when none is specified.
     * }
     */
    public function __construct(UriInterface $host, array $config = [])
    {
        if ('' === $host->getHost()) {
            throw new \LogicException('Host can not be empty');
        }

        $this->host = $host;

        $resolver = new OptionsResolver();
        $this->configureOptions($resolver);
        $options = $resolver->resolve($config);

        $this->replace = $options['replace'];
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        if ($this->replace || '' === $request->getUri()->getHost()) {
            $uri = $request->getUri()
                ->withHost($this->host->getHost())
                ->withScheme($this->host->getScheme())
                ->withPort($this->host->getPort())
            ;

            $request = $request->withUri($uri);
        }

        return $next($request);
    }

    /**
     * @param OptionsResolver $resolver
     */
    private function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'replace' => false,
        ]);
        $resolver->setAllowedTypes('replace', 'bool');
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;

/**
 * Prepend a base path to the request URI. Useful for base API URLs like http://domain.com/api.
 *
 * @author Sullivan Senechal <soullivaneuh@gmail.com>
 */
final class AddPathPlugin implements Plugin
{
    /**
     * @var UriInterface
     */
    private $uri;

    /**
     * Stores identifiers of the already altered requests.
     *
     * @var array
     */
    private $alteredRequests = [];

    /**
     * @param UriInterface $uri
     */
    public function __construct(UriInterface $uri)
    {
        if ('' === $uri->getPath()) {
            throw new \LogicException('URI path cannot be empty');
        }

        if ('/' === substr($uri->getPath(), -1)) {
            $uri = $uri->withPath(rtrim($uri->getPath(), '/'));
        }

        $this->uri = $uri;
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        $identifier = spl_object_hash((object) $first);

        if (!array_key_exists($identifier, $this->alteredRequests)) {
            $request = $request->withUri($request->getUri()
                ->withPath($this->uri->getPath().$request->getUri()->getPath())
            );
            $this->alteredRequests[$identifier] = $identifier;
        }

        return $next($request);
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;

/**
 * Send an authenticated request.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
final class AuthenticationPlugin implements Plugin
{
    /**
     * @var Authentication An authentication system
     */
    private $authentication;

    /**
     * @param Authentication $authentication
     */
    public function __construct(Authentication $authentication)
    {
        $this->authentication = $authentication;
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        $request = $this->authentication->authenticate($request);

        return $next($request);
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;

/**
 * Combines the AddHostPlugin and AddPathPlugin.
 *
 * @author Sullivan Senechal <soullivaneuh@gmail.com>
 */
final class BaseUriPlugin implements Plugin
{
    /**
     * @var AddHostPlugin
     */
    private $addHostPlugin;

    /**
     * @var AddPathPlugin|null
     */
    private $addPathPlugin = null;

    /**
     * @param UriInterface $uri        Has to contain a host name and cans have a path.
     * @param array        $hostConfig Config for AddHostPlugin. @see AddHostPlugin::configureOptions
     */
    public function __construct(UriInterface $uri, array $hostConfig = [])
    {
        $this->addHostPlugin = new AddHostPlugin($uri, $hostConfig);

        if (rtrim($uri->getPath(), '/')) {
            $this->addPathPlugin = new AddPathPlugin($uri);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        $addHostNext = function (RequestInterface $request) use ($next, $first) {
            return $this->addHostPlugin->handleRequest($request, $next, $first);
        };

        if ($this->addPathPlugin) {
            return $this->addPathPlugin->handleRequest($request, $addHostNext, $first);
        }

        return $addHostNext($request);
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Http\Message\Encoding\ChunkStream;
use Psr\Http\Message\RequestInterface;

/**
 * Allow to set the correct content length header on the request or to transfer it as a chunk if not possible.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
final class ContentLengthPlugin implements Plugin
{
    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        if (!$request->hasHeader('Content-Length')) {
            $stream = $request->getBody();

            // Cannot determine the size so we use a chunk stream
            if (null === $stream->getSize()) {
                $stream = new ChunkStream($stream);
                $request = $request->withBody($stream);
                $request = $request->withAddedHeader('Transfer-Encoding', 'chunked');
            } else {
                $request = $request->withHeader('Content-Length', (string) $stream->getSize());
            }
        }

        return $next($request);
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Allow to set the correct content type header on the request automatically only if it is not set.
 *
 * @author Karim Pinchon <karim.pinchon@gmail.com>
 */
final class ContentTypePlugin implements Plugin
{
    /**
     * Allow to disable the content type detection when stream is too large (as it can consume a lot of resource).
     *
     * @var bool
     *
     * true     skip the content type detection
     * false    detect the content type (default value)
     */
    protected $skipDetection;

    /**
     * Determine the size stream limit for which the detection as to be skipped (default to 16Mb).
     *
     * @var int
     */
    protected $sizeLimit;

    /**
     * @param array $config {
     *
     *     @var bool $skip_detection True skip detection if stream size is bigger than $size_limit.
     *     @var int $size_limit size stream limit for which the detection as to be skipped.
     * }
     */
    public function __construct(array $config = [])
    {
        $resolver = new OptionsResolver();
        $resolver->setDefaults([
            'skip_detection' => false,
            'size_limit' => 16000000,
        ]);
        $resolver->setAllowedTypes('skip_detection', 'bool');
        $resolver->setAllowedTypes('size_limit', 'int');

        $options = $resolver->resolve($config);

        $this->skipDetection = $options['skip_detection'];
        $this->sizeLimit = $options['size_limit'];
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        if (!$request->hasHeader('Content-Type')) {
            $stream = $request->getBody();
            $streamSize = $stream->getSize();

            if (!$stream->isSeekable()) {
                return $next($request);
            }

            if (0 === $streamSize) {
                return $next($request);
            }

            if ($this->skipDetection && (null === $streamSize || $streamSize >= $this->sizeLimit)) {
                return $next($request);
            }

            if ($this->isJson($stream)) {
                $request = $request->withHeader('Content-Type', 'application/json');

                return $next($request);
            }

            if ($this->isXml($stream)) {
                $request = $request->withHeader('Content-Type', 'application/xml');

                return $next($request);
            }
        }

        return $next($request);
    }

    /**
     * @param $stream StreamInterface
     *
     * @return bool
     */
    private function isJson($stream)
    {
        $stream->rewind();

        json_decode($stream->getContents());

        return JSON_ERROR_NONE === json_last_error();
    }

    /**
     * @param $stream StreamInterface
     *
     * @return \SimpleXMLElement|false
     */
    private function isXml($stream)
    {
        $stream->rewind();

        $previousValue = libxml_use_internal_errors(true);
        $isXml = simplexml_load_string($stream->getContents());
        libxml_use_internal_errors($previousValue);

        return $isXml;
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Http\Client\Exception\TransferException;
use Http\Message\Cookie;
use Http\Message\CookieJar;
use Http\Message\CookieUtil;
use Http\Message\Exception\UnexpectedValueException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
 * Handle request cookies.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
final class CookiePlugin implements Plugin
{
    /**
     * Cookie storage.
     *
     * @var CookieJar
     */
    private $cookieJar;

    /**
     * @param CookieJar $cookieJar
     */
    public function __construct(CookieJar $cookieJar)
    {
        $this->cookieJar = $cookieJar;
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        $cookies = [];
        foreach ($this->cookieJar->getCookies() as $cookie) {
            if ($cookie->isExpired()) {
                continue;
            }

            if (!$cookie->matchDomain($request->getUri()->getHost())) {
                continue;
            }

            if (!$cookie->matchPath($request->getUri()->getPath())) {
                continue;
            }

            if ($cookie->isSecure() && ('https' !== $request->getUri()->getScheme())) {
                continue;
            }

            $cookies[] = sprintf('%s=%s', $cookie->getName(), $cookie->getValue());
        }

        if (!empty($cookies)) {
            $request = $request->withAddedHeader('Cookie', implode('; ', array_unique($cookies)));
        }

        return $next($request)->then(function (ResponseInterface $response) use ($request) {
            if ($response->hasHeader('Set-Cookie')) {
                $setCookies = $response->getHeader('Set-Cookie');

                foreach ($setCookies as $setCookie) {
                    $cookie = $this->createCookie($request, $setCookie);

                    // Cookie invalid do not use it
                    if (null === $cookie) {
                        continue;
                    }

                    // Restrict setting cookie from another domain
                    if (!preg_match("/\.{$cookie->getDomain()}$/", '.'.$request->getUri()->getHost())) {
                        continue;
                    }

                    $this->cookieJar->addCookie($cookie);
                }
            }

            return $response;
        });
    }

    /**
     * Creates a cookie from a string.
     *
     * @param RequestInterface $request
     * @param $setCookie
     *
     * @return Cookie|null
     *
     * @throws TransferException
     */
    private function createCookie(RequestInterface $request, $setCookie)
    {
        $parts = array_map('trim', explode(';', $setCookie));

        if (empty($parts) || !strpos($parts[0], '=')) {
            return;
        }

        list($name, $cookieValue) = $this->createValueKey(array_shift($parts));

        $maxAge = null;
        $expires = null;
        $domain = $request->getUri()->getHost();
        $path = $request->getUri()->getPath();
        $secure = false;
        $httpOnly = false;

        // Add the cookie pieces into the parsed data array
        foreach ($parts as $part) {
            list($key, $value) = $this->createValueKey($part);

            switch (strtolower($key)) {
                case 'expires':
                    try {
                        $expires = CookieUtil::parseDate($value);
                    } catch (UnexpectedValueException $e) {
                        throw new TransferException(
                            sprintf(
                                'Cookie header `%s` expires value `%s` could not be converted to date',
                                $name,
                                $value
                            ),
                            0,
                            $e
                        );
                    }

                    break;

                case 'max-age':
                    $maxAge = (int) $value;

                    break;

                case 'domain':
                    $domain = $value;

                    break;

                case 'path':
                    $path = $value;

                    break;

                case 'secure':
                    $secure = true;

                    break;

                case 'httponly':
                    $httpOnly = true;

                    break;
            }
        }

        return new Cookie($name, $cookieValue, $maxAge, $domain, $path, $secure, $httpOnly, $expires);
    }

    /**
     * Separates key/value pair from cookie.
     *
     * @param $part
     *
     * @return array
     */
    private function createValueKey($part)
    {
        $parts = explode('=', $part, 2);
        $key = trim($parts[0]);
        $value = isset($parts[1]) ? trim($parts[1]) : true;

        return [$key, $value];
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Http\Message\Encoding;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Allow to decode response body with a chunk, deflate, compress or gzip encoding.
 *
 * If zlib is not installed, only chunked encoding can be handled.
 *
 * If Content-Encoding is not disabled, the plugin will add an Accept-Encoding header for the encoding methods it supports.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
final class DecoderPlugin implements Plugin
{
    /**
     * @var bool Whether this plugin decode stream with value in the Content-Encoding header (default to true).
     *
     * If set to false only the Transfer-Encoding header will be used
     */
    private $useContentEncoding;

    /**
     * @param array $config {
     *
     *    @var bool $use_content_encoding Whether this plugin should look at the Content-Encoding header first or only at the Transfer-Encoding (defaults to true).
     * }
     */
    public function __construct(array $config = [])
    {
        $resolver = new OptionsResolver();
        $resolver->setDefaults([
            'use_content_encoding' => true,
        ]);
        $resolver->setAllowedTypes('use_content_encoding', 'bool');
        $options = $resolver->resolve($config);

        $this->useContentEncoding = $options['use_content_encoding'];
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        $encodings = extension_loaded('zlib') ? ['gzip', 'deflate'] : ['identity'];

        if ($this->useContentEncoding) {
            $request = $request->withHeader('Accept-Encoding', $encodings);
        }
        $encodings[] = 'chunked';
        $request = $request->withHeader('TE', $encodings);

        return $next($request)->then(function (ResponseInterface $response) {
            return $this->decodeResponse($response);
        });
    }

    /**
     * Decode a response body given its Transfer-Encoding or Content-Encoding value.
     *
     * @param ResponseInterface $response Response to decode
     *
     * @return ResponseInterface New response decoded
     */
    private function decodeResponse(ResponseInterface $response)
    {
        $response = $this->decodeOnEncodingHeader('Transfer-Encoding', $response);

        if ($this->useContentEncoding) {
            $response = $this->decodeOnEncodingHeader('Content-Encoding', $response);
        }

        return $response;
    }

    /**
     * Decode a response on a specific header (content encoding or transfer encoding mainly).
     *
     * @param string            $headerName Name of the header
     * @param ResponseInterface $response   Response
     *
     * @return ResponseInterface A new instance of the response decoded
     */
    private function decodeOnEncodingHeader($headerName, ResponseInterface $response)
    {
        if ($response->hasHeader($headerName)) {
            $encodings = $response->getHeader($headerName);
            $newEncodings = [];

            while ($encoding = array_pop($encodings)) {
                $stream = $this->decorateStream($encoding, $response->getBody());

                if (false === $stream) {
                    array_unshift($newEncodings, $encoding);

                    continue;
                }

                $response = $response->withBody($stream);
            }

            if (\count($newEncodings) > 0) {
                $response = $response->withHeader($headerName, $newEncodings);
            } else {
                $response = $response->withoutHeader($headerName);
            }
        }

        return $response;
    }

    /**
     * Decorate a stream given an encoding.
     *
     * @param string          $encoding
     * @param StreamInterface $stream
     *
     * @return StreamInterface|false A new stream interface or false if encoding is not supported
     */
    private function decorateStream($encoding, StreamInterface $stream)
    {
        if ('chunked' === strtolower($encoding)) {
            return new Encoding\DechunkStream($stream);
        }

        if ('deflate' === strtolower($encoding)) {
            return new Encoding\DecompressStream($stream);
        }

        if ('gzip' === strtolower($encoding)) {
            return new Encoding\GzipDecodeStream($stream);
        }

        return false;
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Exception\ClientErrorException;
use Http\Client\Common\Exception\ServerErrorException;
use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Throw exception when the response of a request is not acceptable.
 *
 * Status codes 400-499 lead to a ClientErrorException, status 500-599 to a ServerErrorException.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
final class ErrorPlugin implements Plugin
{
    /**
     * @var bool Whether this plugin should only throw 5XX Exceptions (default to false).
     *
     * If set to true 4XX Responses code will never throw an exception
     */
    private $onlyServerException;

    /**
     * @param array $config {
     *
     *    @var bool only_server_exception Whether this plugin should only throw 5XX Exceptions (default to false).
     * }
     */
    public function __construct(array $config = [])
    {
        $resolver = new OptionsResolver();
        $resolver->setDefaults([
            'only_server_exception' => false,
        ]);
        $resolver->setAllowedTypes('only_server_exception', 'bool');
        $options = $resolver->resolve($config);

        $this->onlyServerException = $options['only_server_exception'];
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        $promise = $next($request);

        return $promise->then(function (ResponseInterface $response) use ($request) {
            return $this->transformResponseToException($request, $response);
        });
    }

    /**
     * Transform response to an error if possible.
     *
     * @param RequestInterface  $request  Request of the call
     * @param ResponseInterface $response Response of the call
     *
     * @throws ClientErrorException If response status code is a 4xx
     * @throws ServerErrorException If response status code is a 5xx
     *
     * @return ResponseInterface If status code is not in 4xx or 5xx return response
     */
    protected function transformResponseToException(RequestInterface $request, ResponseInterface $response)
    {
        if (!$this->onlyServerException && $response->getStatusCode() >= 400 && $response->getStatusCode() < 500) {
            throw new ClientErrorException($response->getReasonPhrase(), $request, $response);
        }

        if ($response->getStatusCode() >= 500 && $response->getStatusCode() < 600) {
            throw new ServerErrorException($response->getReasonPhrase(), $request, $response);
        }

        return $response;
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;

/**
 * Append headers to the request.
 *
 * If the header already exists the value will be appended to the current value.
 *
 * This only makes sense for headers that can have multiple values like 'Forwarded'
 *
 * @see https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
 *
 * @author Soufiane Ghzal <sghzal@gmail.com>
 */
final class HeaderAppendPlugin implements Plugin
{
    /**
     * @var array
     */
    private $headers = [];

    /**
     * @param array $headers Hashmap of header name to header value
     */
    public function __construct(array $headers)
    {
        $this->headers = $headers;
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        foreach ($this->headers as $header => $headerValue) {
            $request = $request->withAddedHeader($header, $headerValue);
        }

        return $next($request);
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;

/**
 * Set header to default value if it does not exist.
 *
 * If a given header already exists the value wont be replaced and the request wont be changed.
 *
 * @author Soufiane Ghzal <sghzal@gmail.com>
 */
final class HeaderDefaultsPlugin implements Plugin
{
    /**
     * @var array
     */
    private $headers = [];

    /**
     * @param array $headers Hashmap of header name to header value
     */
    public function __construct(array $headers)
    {
        $this->headers = $headers;
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        foreach ($this->headers as $header => $headerValue) {
            if (!$request->hasHeader($header)) {
                $request = $request->withHeader($header, $headerValue);
            }
        }

        return $next($request);
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;

/**
 * Removes headers from the request.
 *
 * @author Soufiane Ghzal <sghzal@gmail.com>
 */
final class HeaderRemovePlugin implements Plugin
{
    /**
     * @var array
     */
    private $headers = [];

    /**
     * @param array $headers List of header names to remove from the request
     */
    public function __construct(array $headers)
    {
        $this->headers = $headers;
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        foreach ($this->headers as $header) {
            if ($request->hasHeader($header)) {
                $request = $request->withoutHeader($header);
            }
        }

        return $next($request);
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;

/**
 * Set headers on the request.
 *
 * If the header does not exist it wil be set, if the header already exists it will be replaced.
 *
 * @author Soufiane Ghzal <sghzal@gmail.com>
 */
final class HeaderSetPlugin implements Plugin
{
    /**
     * @var array
     */
    private $headers = [];

    /**
     * @param array $headers Hashmap of header name to header value
     */
    public function __construct(array $headers)
    {
        $this->headers = $headers;
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        foreach ($this->headers as $header => $headerValue) {
            $request = $request->withHeader($header, $headerValue);
        }

        return $next($request);
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Http\Client\Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
 * Record HTTP calls.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
final class HistoryPlugin implements Plugin
{
    /**
     * Journal use to store request / responses / exception.
     *
     * @var Journal
     */
    private $journal;

    /**
     * @param Journal $journal
     */
    public function __construct(Journal $journal)
    {
        $this->journal = $journal;
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        $journal = $this->journal;

        return $next($request)->then(function (ResponseInterface $response) use ($request, $journal) {
            $journal->addSuccess($request, $response);

            return $response;
        }, function (Exception $exception) use ($request, $journal) {
            $journal->addFailure($request, $exception);

            throw $exception;
        });
    }
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
 * Records history of HTTP calls.
 *
 * @author Joel Wurtz <joel.wurtz@gmail.com>
 */
interface Journal
{
    /**
     * Record a successful call.
     *
     * @param RequestInterface  $request  Request use to make the call
     * @param ResponseInterface $response Response returned by the call
     */
    public function addSuccess(RequestInterface $request, ResponseInterface $response);

    /**
     * Record a failed call.
     *
     * @param RequestInterface $request   Request use to make the call
     * @param Exception        $exception Exception returned by the call
     */
    public function addFailure(RequestInterface $request, Exception $exception);
}


code:
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Psr\Http\Message\RequestInterface;

/**
 * Set query to default value if it does not exist.
 *
 * If a given query parameter already exists the value wont be replaced and the request wont be changed.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
final class QueryDefaultsPlugin implements Plugin
{
    /**
     * @var array
     */
    private $queryParams = [];

    /**
     * @param array $queryParams Hashmap of query name to query value. Names and values must not be url encoded as
     *                           this plugin will encode them
     */
    public function __construct(array $queryParams)
    {
        $this->queryParams = $queryParams;
    }

    /**
     * {@inheritdoc}
     */
    public function handleRequest(RequestInterface $request, callable $next, callable $first)
    {
        $uri = $request->getUri();

        parse_str($uri->getQuery(), $query);
        $query += $this->queryParams;

        $request = $request->withUri(
            $uri->withQuery(http_build_query($query))
        );

        return $next($request);
    }
}