129 changed files with 21435 additions and 0 deletions
@ -0,0 +1,53 @@ |
|||||
|
<?php |
||||
|
/** |
||||
|
* 自动扣减天数计算权益 |
||||
|
*/ |
||||
|
|
||||
|
define('IN_UNIAPP', true); |
||||
|
header('Access-Control-Allow-Origin:*'); |
||||
|
if(is_file('../../../wlversion.txt')){ |
||||
|
$version = file_get_contents('../../../wlversion.txt'); |
||||
|
define("MODULE_NAME",$version); |
||||
|
}else{ |
||||
|
define("MODULE_NAME",'weliam_smartcity'); |
||||
|
} |
||||
|
require '../../../framework/bootstrap.inc.php'; |
||||
|
require '../../../addons/'.MODULE_NAME.'/core/common/defines.php'; |
||||
|
require_once IA_ROOT . '/framework/bootstrap.inc.php'; |
||||
|
require_once IA_ROOT . '/addons/'.MODULE_NAME.'/core/model/Rights.mod.php'; |
||||
|
|
||||
|
$start_time = time(); |
||||
|
#error_reporting(-1); |
||||
|
#ini_set('display_errors',1); |
||||
|
$h = $_GET['h'] ? : date("H"); |
||||
|
|
||||
|
if (!in_array($h,['23','00','01'])) exit('时间未到'); |
||||
|
|
||||
|
$rights = pdo_getall(PDO_NAME.'rights',['status' => 1,'calculate_method' => 1],['id']); |
||||
|
|
||||
|
foreach ($rights as $rightsValue) { |
||||
|
|
||||
|
$rightsid = $rightsValue['id']; |
||||
|
|
||||
|
$rightsUse = pdo_getall(PDO_NAME.'member_rights_use',['rightsid' => $rightsid, 'status' => [0,1]]); |
||||
|
if (!$rightsUse) continue; |
||||
|
|
||||
|
foreach ($rightsUse as $rightsUseValue) { |
||||
|
if ($rightsid == 1 && $rightsUseValue['status'] == 0) continue; // 广告曝光未开始不计算天数 |
||||
|
$isTopRes = 0; |
||||
|
if ($rightsid == 2) { // 置顶后开始计算天数 |
||||
|
$isTopRes = Rights::recruitIsTop($rightsUseValue['relation_id'],$rightsUse['rights_memberid'],$rightsUse['use_total_amount']-$rightsUse['use_amount']); |
||||
|
if (!$isTopRes) continue; |
||||
|
} |
||||
|
$num = 1; |
||||
|
if ($isTopRes == 2) $num = 0; |
||||
|
$memberRights = pdo_get(PDO_NAME . 'member_rights',['id' => $rightsUseValue['member_rightsid']],['mid']); |
||||
|
Rights::useMemberRights($rightsid,$memberRights['mid'],$rightsUseValue['relation_id'],$rightsUseValue['relation_type'],$num); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
$end_time = time(); |
||||
|
|
||||
|
echo ceil(($end_time - $start_time)*1000) . "s"; |
||||
|
exit; |
||||
@ -0,0 +1,445 @@ |
|||||
|
<?php |
||||
|
|
||||
|
/* |
||||
|
* This file is part of Composer. |
||||
|
* |
||||
|
* (c) Nils Adermann <naderman@naderman.de> |
||||
|
* Jordi Boggiano <j.boggiano@seld.be> |
||||
|
* |
||||
|
* For the full copyright and license information, please view the LICENSE |
||||
|
* file that was distributed with this source code. |
||||
|
*/ |
||||
|
|
||||
|
namespace Composer\Autoload; |
||||
|
|
||||
|
/** |
||||
|
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader. |
||||
|
* |
||||
|
* $loader = new \Composer\Autoload\ClassLoader(); |
||||
|
* |
||||
|
* // register classes with namespaces |
||||
|
* $loader->add('Symfony\Component', __DIR__.'/component'); |
||||
|
* $loader->add('Symfony', __DIR__.'/framework'); |
||||
|
* |
||||
|
* // activate the autoloader |
||||
|
* $loader->register(); |
||||
|
* |
||||
|
* // to enable searching the include path (eg. for PEAR packages) |
||||
|
* $loader->setUseIncludePath(true); |
||||
|
* |
||||
|
* In this example, if you try to use a class in the Symfony\Component |
||||
|
* namespace or one of its children (Symfony\Component\Console for instance), |
||||
|
* the autoloader will first look for the class under the component/ |
||||
|
* directory, and it will then fallback to the framework/ directory if not |
||||
|
* found before giving up. |
||||
|
* |
||||
|
* This class is loosely based on the Symfony UniversalClassLoader. |
||||
|
* |
||||
|
* @author Fabien Potencier <fabien@symfony.com> |
||||
|
* @author Jordi Boggiano <j.boggiano@seld.be> |
||||
|
* @see http://www.php-fig.org/psr/psr-0/ |
||||
|
* @see http://www.php-fig.org/psr/psr-4/ |
||||
|
*/ |
||||
|
class ClassLoader |
||||
|
{ |
||||
|
// PSR-4 |
||||
|
private $prefixLengthsPsr4 = array(); |
||||
|
private $prefixDirsPsr4 = array(); |
||||
|
private $fallbackDirsPsr4 = array(); |
||||
|
|
||||
|
// PSR-0 |
||||
|
private $prefixesPsr0 = array(); |
||||
|
private $fallbackDirsPsr0 = array(); |
||||
|
|
||||
|
private $useIncludePath = false; |
||||
|
private $classMap = array(); |
||||
|
private $classMapAuthoritative = false; |
||||
|
private $missingClasses = array(); |
||||
|
private $apcuPrefix; |
||||
|
|
||||
|
public function getPrefixes() |
||||
|
{ |
||||
|
if (!empty($this->prefixesPsr0)) { |
||||
|
return call_user_func_array('array_merge', $this->prefixesPsr0); |
||||
|
} |
||||
|
|
||||
|
return array(); |
||||
|
} |
||||
|
|
||||
|
public function getPrefixesPsr4() |
||||
|
{ |
||||
|
return $this->prefixDirsPsr4; |
||||
|
} |
||||
|
|
||||
|
public function getFallbackDirs() |
||||
|
{ |
||||
|
return $this->fallbackDirsPsr0; |
||||
|
} |
||||
|
|
||||
|
public function getFallbackDirsPsr4() |
||||
|
{ |
||||
|
return $this->fallbackDirsPsr4; |
||||
|
} |
||||
|
|
||||
|
public function getClassMap() |
||||
|
{ |
||||
|
return $this->classMap; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param array $classMap Class to filename map |
||||
|
*/ |
||||
|
public function addClassMap(array $classMap) |
||||
|
{ |
||||
|
if ($this->classMap) { |
||||
|
$this->classMap = array_merge($this->classMap, $classMap); |
||||
|
} else { |
||||
|
$this->classMap = $classMap; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Registers a set of PSR-0 directories for a given prefix, either |
||||
|
* appending or prepending to the ones previously set for this prefix. |
||||
|
* |
||||
|
* @param string $prefix The prefix |
||||
|
* @param array|string $paths The PSR-0 root directories |
||||
|
* @param bool $prepend Whether to prepend the directories |
||||
|
*/ |
||||
|
public function add($prefix, $paths, $prepend = false) |
||||
|
{ |
||||
|
if (!$prefix) { |
||||
|
if ($prepend) { |
||||
|
$this->fallbackDirsPsr0 = array_merge( |
||||
|
(array) $paths, |
||||
|
$this->fallbackDirsPsr0 |
||||
|
); |
||||
|
} else { |
||||
|
$this->fallbackDirsPsr0 = array_merge( |
||||
|
$this->fallbackDirsPsr0, |
||||
|
(array) $paths |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
$first = $prefix[0]; |
||||
|
if (!isset($this->prefixesPsr0[$first][$prefix])) { |
||||
|
$this->prefixesPsr0[$first][$prefix] = (array) $paths; |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
if ($prepend) { |
||||
|
$this->prefixesPsr0[$first][$prefix] = array_merge( |
||||
|
(array) $paths, |
||||
|
$this->prefixesPsr0[$first][$prefix] |
||||
|
); |
||||
|
} else { |
||||
|
$this->prefixesPsr0[$first][$prefix] = array_merge( |
||||
|
$this->prefixesPsr0[$first][$prefix], |
||||
|
(array) $paths |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Registers a set of PSR-4 directories for a given namespace, either |
||||
|
* appending or prepending to the ones previously set for this namespace. |
||||
|
* |
||||
|
* @param string $prefix The prefix/namespace, with trailing '\\' |
||||
|
* @param array|string $paths The PSR-4 base directories |
||||
|
* @param bool $prepend Whether to prepend the directories |
||||
|
* |
||||
|
* @throws \InvalidArgumentException |
||||
|
*/ |
||||
|
public function addPsr4($prefix, $paths, $prepend = false) |
||||
|
{ |
||||
|
if (!$prefix) { |
||||
|
// Register directories for the root namespace. |
||||
|
if ($prepend) { |
||||
|
$this->fallbackDirsPsr4 = array_merge( |
||||
|
(array) $paths, |
||||
|
$this->fallbackDirsPsr4 |
||||
|
); |
||||
|
} else { |
||||
|
$this->fallbackDirsPsr4 = array_merge( |
||||
|
$this->fallbackDirsPsr4, |
||||
|
(array) $paths |
||||
|
); |
||||
|
} |
||||
|
} elseif (!isset($this->prefixDirsPsr4[$prefix])) { |
||||
|
// Register directories for a new namespace. |
||||
|
$length = strlen($prefix); |
||||
|
if ('\\' !== $prefix[$length - 1]) { |
||||
|
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); |
||||
|
} |
||||
|
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; |
||||
|
$this->prefixDirsPsr4[$prefix] = (array) $paths; |
||||
|
} elseif ($prepend) { |
||||
|
// Prepend directories for an already registered namespace. |
||||
|
$this->prefixDirsPsr4[$prefix] = array_merge( |
||||
|
(array) $paths, |
||||
|
$this->prefixDirsPsr4[$prefix] |
||||
|
); |
||||
|
} else { |
||||
|
// Append directories for an already registered namespace. |
||||
|
$this->prefixDirsPsr4[$prefix] = array_merge( |
||||
|
$this->prefixDirsPsr4[$prefix], |
||||
|
(array) $paths |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Registers a set of PSR-0 directories for a given prefix, |
||||
|
* replacing any others previously set for this prefix. |
||||
|
* |
||||
|
* @param string $prefix The prefix |
||||
|
* @param array|string $paths The PSR-0 base directories |
||||
|
*/ |
||||
|
public function set($prefix, $paths) |
||||
|
{ |
||||
|
if (!$prefix) { |
||||
|
$this->fallbackDirsPsr0 = (array) $paths; |
||||
|
} else { |
||||
|
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Registers a set of PSR-4 directories for a given namespace, |
||||
|
* replacing any others previously set for this namespace. |
||||
|
* |
||||
|
* @param string $prefix The prefix/namespace, with trailing '\\' |
||||
|
* @param array|string $paths The PSR-4 base directories |
||||
|
* |
||||
|
* @throws \InvalidArgumentException |
||||
|
*/ |
||||
|
public function setPsr4($prefix, $paths) |
||||
|
{ |
||||
|
if (!$prefix) { |
||||
|
$this->fallbackDirsPsr4 = (array) $paths; |
||||
|
} else { |
||||
|
$length = strlen($prefix); |
||||
|
if ('\\' !== $prefix[$length - 1]) { |
||||
|
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); |
||||
|
} |
||||
|
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; |
||||
|
$this->prefixDirsPsr4[$prefix] = (array) $paths; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Turns on searching the include path for class files. |
||||
|
* |
||||
|
* @param bool $useIncludePath |
||||
|
*/ |
||||
|
public function setUseIncludePath($useIncludePath) |
||||
|
{ |
||||
|
$this->useIncludePath = $useIncludePath; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Can be used to check if the autoloader uses the include path to check |
||||
|
* for classes. |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function getUseIncludePath() |
||||
|
{ |
||||
|
return $this->useIncludePath; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Turns off searching the prefix and fallback directories for classes |
||||
|
* that have not been registered with the class map. |
||||
|
* |
||||
|
* @param bool $classMapAuthoritative |
||||
|
*/ |
||||
|
public function setClassMapAuthoritative($classMapAuthoritative) |
||||
|
{ |
||||
|
$this->classMapAuthoritative = $classMapAuthoritative; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Should class lookup fail if not found in the current class map? |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function isClassMapAuthoritative() |
||||
|
{ |
||||
|
return $this->classMapAuthoritative; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* APCu prefix to use to cache found/not-found classes, if the extension is enabled. |
||||
|
* |
||||
|
* @param string|null $apcuPrefix |
||||
|
*/ |
||||
|
public function setApcuPrefix($apcuPrefix) |
||||
|
{ |
||||
|
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* The APCu prefix in use, or null if APCu caching is not enabled. |
||||
|
* |
||||
|
* @return string|null |
||||
|
*/ |
||||
|
public function getApcuPrefix() |
||||
|
{ |
||||
|
return $this->apcuPrefix; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Registers this instance as an autoloader. |
||||
|
* |
||||
|
* @param bool $prepend Whether to prepend the autoloader or not |
||||
|
*/ |
||||
|
public function register($prepend = false) |
||||
|
{ |
||||
|
spl_autoload_register(array($this, 'loadClass'), true, $prepend); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Unregisters this instance as an autoloader. |
||||
|
*/ |
||||
|
public function unregister() |
||||
|
{ |
||||
|
spl_autoload_unregister(array($this, 'loadClass')); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Loads the given class or interface. |
||||
|
* |
||||
|
* @param string $class The name of the class |
||||
|
* @return bool|null True if loaded, null otherwise |
||||
|
*/ |
||||
|
public function loadClass($class) |
||||
|
{ |
||||
|
if ($file = $this->findFile($class)) { |
||||
|
includeFile($file); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Finds the path to the file where the class is defined. |
||||
|
* |
||||
|
* @param string $class The name of the class |
||||
|
* |
||||
|
* @return string|false The path if found, false otherwise |
||||
|
*/ |
||||
|
public function findFile($class) |
||||
|
{ |
||||
|
// class map lookup |
||||
|
if (isset($this->classMap[$class])) { |
||||
|
return $this->classMap[$class]; |
||||
|
} |
||||
|
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { |
||||
|
return false; |
||||
|
} |
||||
|
if (null !== $this->apcuPrefix) { |
||||
|
$file = apcu_fetch($this->apcuPrefix.$class, $hit); |
||||
|
if ($hit) { |
||||
|
return $file; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$file = $this->findFileWithExtension($class, '.php'); |
||||
|
|
||||
|
// Search for Hack files if we are running on HHVM |
||||
|
if (false === $file && defined('HHVM_VERSION')) { |
||||
|
$file = $this->findFileWithExtension($class, '.hh'); |
||||
|
} |
||||
|
|
||||
|
if (null !== $this->apcuPrefix) { |
||||
|
apcu_add($this->apcuPrefix.$class, $file); |
||||
|
} |
||||
|
|
||||
|
if (false === $file) { |
||||
|
// Remember that this class does not exist. |
||||
|
$this->missingClasses[$class] = true; |
||||
|
} |
||||
|
|
||||
|
return $file; |
||||
|
} |
||||
|
|
||||
|
private function findFileWithExtension($class, $ext) |
||||
|
{ |
||||
|
// PSR-4 lookup |
||||
|
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; |
||||
|
|
||||
|
$first = $class[0]; |
||||
|
if (isset($this->prefixLengthsPsr4[$first])) { |
||||
|
$subPath = $class; |
||||
|
while (false !== $lastPos = strrpos($subPath, '\\')) { |
||||
|
$subPath = substr($subPath, 0, $lastPos); |
||||
|
$search = $subPath . '\\'; |
||||
|
if (isset($this->prefixDirsPsr4[$search])) { |
||||
|
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); |
||||
|
foreach ($this->prefixDirsPsr4[$search] as $dir) { |
||||
|
if (file_exists($file = $dir . $pathEnd)) { |
||||
|
return $file; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// PSR-4 fallback dirs |
||||
|
foreach ($this->fallbackDirsPsr4 as $dir) { |
||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { |
||||
|
return $file; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// PSR-0 lookup |
||||
|
if (false !== $pos = strrpos($class, '\\')) { |
||||
|
// namespaced class name |
||||
|
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) |
||||
|
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); |
||||
|
} else { |
||||
|
// PEAR-like class name |
||||
|
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; |
||||
|
} |
||||
|
|
||||
|
if (isset($this->prefixesPsr0[$first])) { |
||||
|
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { |
||||
|
if (0 === strpos($class, $prefix)) { |
||||
|
foreach ($dirs as $dir) { |
||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { |
||||
|
return $file; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// PSR-0 fallback dirs |
||||
|
foreach ($this->fallbackDirsPsr0 as $dir) { |
||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { |
||||
|
return $file; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// PSR-0 include paths. |
||||
|
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { |
||||
|
return $file; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Scope isolated include. |
||||
|
* |
||||
|
* Prevents access to $this/self from included files. |
||||
|
*/ |
||||
|
function includeFile($file) |
||||
|
{ |
||||
|
include $file; |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
|
||||
|
Copyright (c) Nils Adermann, Jordi Boggiano |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is furnished |
||||
|
to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in all |
||||
|
copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
|
THE SOFTWARE. |
||||
|
|
||||
@ -0,0 +1,9 @@ |
|||||
|
<?php |
||||
|
|
||||
|
// autoload_classmap.php @generated by Composer |
||||
|
|
||||
|
$vendorDir = dirname(dirname(__FILE__)); |
||||
|
$baseDir = dirname($vendorDir); |
||||
|
|
||||
|
return array( |
||||
|
); |
||||
@ -0,0 +1,19 @@ |
|||||
|
<?php |
||||
|
|
||||
|
// autoload_files.php @generated by Composer |
||||
|
|
||||
|
$vendorDir = dirname(dirname(__FILE__)); |
||||
|
$baseDir = dirname($vendorDir); |
||||
|
|
||||
|
return array( |
||||
|
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', |
||||
|
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php', |
||||
|
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php', |
||||
|
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', |
||||
|
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php', |
||||
|
'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php', |
||||
|
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', |
||||
|
'f0e7e63bbb278a92db02393536748c5f' => $vendorDir . '/overtrue/wechat/src/Kernel/Support/Helpers.php', |
||||
|
'6747f579ad6817f318cc3a7e7a0abb93' => $vendorDir . '/overtrue/wechat/src/Kernel/Helpers.php', |
||||
|
'841780ea2e1d6545ea3a253239d59c05' => $vendorDir . '/qiniu/php-sdk/src/Qiniu/functions.php', |
||||
|
); |
||||
@ -0,0 +1,10 @@ |
|||||
|
<?php |
||||
|
|
||||
|
// autoload_namespaces.php @generated by Composer |
||||
|
|
||||
|
$vendorDir = dirname(dirname(__FILE__)); |
||||
|
$baseDir = dirname($vendorDir); |
||||
|
|
||||
|
return array( |
||||
|
'Pimple' => array($vendorDir . '/pimple/pimple/src'), |
||||
|
); |
||||
@ -0,0 +1,38 @@ |
|||||
|
<?php |
||||
|
|
||||
|
// autoload_psr4.php @generated by Composer |
||||
|
|
||||
|
$vendorDir = dirname(dirname(__FILE__)); |
||||
|
$baseDir = dirname($vendorDir); |
||||
|
|
||||
|
return array( |
||||
|
'Yansongda\\Supports\\' => array($vendorDir . '/yansongda/supports/src'), |
||||
|
'Yansongda\\Pay\\' => array($vendorDir . '/yansongda/pay/src'), |
||||
|
'Workerman\\MySQL\\' => array($vendorDir . '/workerman/mysql/src'), |
||||
|
'Workerman\\' => array($vendorDir . '/workerman/workerman'), |
||||
|
'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'), |
||||
|
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), |
||||
|
'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'), |
||||
|
'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'), |
||||
|
'Symfony\\Contracts\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher-contracts'), |
||||
|
'Symfony\\Contracts\\Cache\\' => array($vendorDir . '/symfony/cache-contracts'), |
||||
|
'Symfony\\Component\\VarExporter\\' => array($vendorDir . '/symfony/var-exporter'), |
||||
|
'Symfony\\Component\\Mime\\' => array($vendorDir . '/symfony/mime'), |
||||
|
'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'), |
||||
|
'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'), |
||||
|
'Symfony\\Component\\Cache\\' => array($vendorDir . '/symfony/cache'), |
||||
|
'Symfony\\Bridge\\PsrHttpMessage\\' => array($vendorDir . '/symfony/psr-http-message-bridge'), |
||||
|
'Qiniu\\' => array($vendorDir . '/qiniu/php-sdk/src/Qiniu'), |
||||
|
'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'), |
||||
|
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), |
||||
|
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'), |
||||
|
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), |
||||
|
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), |
||||
|
'Overtrue\\Socialite\\' => array($vendorDir . '/overtrue/socialite/src'), |
||||
|
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'), |
||||
|
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'), |
||||
|
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'), |
||||
|
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'), |
||||
|
'EasyWeChat\\' => array($vendorDir . '/overtrue/wechat/src'), |
||||
|
'EasyWeChatComposer\\' => array($vendorDir . '/easywechat-composer/easywechat-composer/src'), |
||||
|
); |
||||
@ -0,0 +1,73 @@ |
|||||
|
<?php |
||||
|
|
||||
|
// autoload_real.php @generated by Composer |
||||
|
|
||||
|
class ComposerAutoloaderInitabe3555d91d8f0317ed20df0cce34e14 |
||||
|
{ |
||||
|
private static $loader; |
||||
|
|
||||
|
public static function loadClassLoader($class) |
||||
|
{ |
||||
|
if ('Composer\Autoload\ClassLoader' === $class) { |
||||
|
require __DIR__ . '/ClassLoader.php'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return \Composer\Autoload\ClassLoader |
||||
|
*/ |
||||
|
public static function getLoader() |
||||
|
{ |
||||
|
if (null !== self::$loader) { |
||||
|
return self::$loader; |
||||
|
} |
||||
|
|
||||
|
spl_autoload_register(array('ComposerAutoloaderInitabe3555d91d8f0317ed20df0cce34e14', 'loadClassLoader'), true, true); |
||||
|
self::$loader = $loader = new \Composer\Autoload\ClassLoader(); |
||||
|
spl_autoload_unregister(array('ComposerAutoloaderInitabe3555d91d8f0317ed20df0cce34e14', 'loadClassLoader')); |
||||
|
|
||||
|
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); |
||||
|
if ($useStaticLoader) { |
||||
|
require_once __DIR__ . '/autoload_static.php'; |
||||
|
|
||||
|
call_user_func(\Composer\Autoload\ComposerStaticInitabe3555d91d8f0317ed20df0cce34e14::getInitializer($loader)); |
||||
|
} else { |
||||
|
$map = require __DIR__ . '/autoload_namespaces.php'; |
||||
|
foreach ($map as $namespace => $path) { |
||||
|
$loader->set($namespace, $path); |
||||
|
} |
||||
|
|
||||
|
$map = require __DIR__ . '/autoload_psr4.php'; |
||||
|
foreach ($map as $namespace => $path) { |
||||
|
$loader->setPsr4($namespace, $path); |
||||
|
} |
||||
|
|
||||
|
$classMap = require __DIR__ . '/autoload_classmap.php'; |
||||
|
if ($classMap) { |
||||
|
$loader->addClassMap($classMap); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$loader->register(true); |
||||
|
|
||||
|
if ($useStaticLoader) { |
||||
|
$includeFiles = Composer\Autoload\ComposerStaticInitabe3555d91d8f0317ed20df0cce34e14::$files; |
||||
|
} else { |
||||
|
$includeFiles = require __DIR__ . '/autoload_files.php'; |
||||
|
} |
||||
|
foreach ($includeFiles as $fileIdentifier => $file) { |
||||
|
composerRequireabe3555d91d8f0317ed20df0cce34e14($fileIdentifier, $file); |
||||
|
} |
||||
|
|
||||
|
return $loader; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function composerRequireabe3555d91d8f0317ed20df0cce34e14($fileIdentifier, $file) |
||||
|
{ |
||||
|
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { |
||||
|
require $file; |
||||
|
|
||||
|
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,219 @@ |
|||||
|
<?php |
||||
|
|
||||
|
// autoload_static.php @generated by Composer |
||||
|
|
||||
|
namespace Composer\Autoload; |
||||
|
|
||||
|
class ComposerStaticInitabe3555d91d8f0317ed20df0cce34e14 |
||||
|
{ |
||||
|
public static $files = array ( |
||||
|
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', |
||||
|
'25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php', |
||||
|
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php', |
||||
|
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', |
||||
|
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php', |
||||
|
'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php', |
||||
|
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', |
||||
|
'f0e7e63bbb278a92db02393536748c5f' => __DIR__ . '/..' . '/overtrue/wechat/src/Kernel/Support/Helpers.php', |
||||
|
'6747f579ad6817f318cc3a7e7a0abb93' => __DIR__ . '/..' . '/overtrue/wechat/src/Kernel/Helpers.php', |
||||
|
'841780ea2e1d6545ea3a253239d59c05' => __DIR__ . '/..' . '/qiniu/php-sdk/src/Qiniu/functions.php', |
||||
|
); |
||||
|
|
||||
|
public static $prefixLengthsPsr4 = array ( |
||||
|
'Y' => |
||||
|
array ( |
||||
|
'Yansongda\\Supports\\' => 19, |
||||
|
'Yansongda\\Pay\\' => 14, |
||||
|
), |
||||
|
'W' => |
||||
|
array ( |
||||
|
'Workerman\\MySQL\\' => 16, |
||||
|
'Workerman\\' => 10, |
||||
|
), |
||||
|
'S' => |
||||
|
array ( |
||||
|
'Symfony\\Polyfill\\Php72\\' => 23, |
||||
|
'Symfony\\Polyfill\\Mbstring\\' => 26, |
||||
|
'Symfony\\Polyfill\\Intl\\Idn\\' => 26, |
||||
|
'Symfony\\Contracts\\Service\\' => 26, |
||||
|
'Symfony\\Contracts\\EventDispatcher\\' => 34, |
||||
|
'Symfony\\Contracts\\Cache\\' => 24, |
||||
|
'Symfony\\Component\\VarExporter\\' => 30, |
||||
|
'Symfony\\Component\\Mime\\' => 23, |
||||
|
'Symfony\\Component\\HttpFoundation\\' => 33, |
||||
|
'Symfony\\Component\\EventDispatcher\\' => 34, |
||||
|
'Symfony\\Component\\Cache\\' => 24, |
||||
|
'Symfony\\Bridge\\PsrHttpMessage\\' => 30, |
||||
|
), |
||||
|
'Q' => |
||||
|
array ( |
||||
|
'Qiniu\\' => 6, |
||||
|
), |
||||
|
'P' => |
||||
|
array ( |
||||
|
'Psr\\SimpleCache\\' => 16, |
||||
|
'Psr\\Log\\' => 8, |
||||
|
'Psr\\Http\\Message\\' => 17, |
||||
|
'Psr\\Container\\' => 14, |
||||
|
'Psr\\Cache\\' => 10, |
||||
|
), |
||||
|
'O' => |
||||
|
array ( |
||||
|
'Overtrue\\Socialite\\' => 19, |
||||
|
), |
||||
|
'M' => |
||||
|
array ( |
||||
|
'Monolog\\' => 8, |
||||
|
), |
||||
|
'G' => |
||||
|
array ( |
||||
|
'GuzzleHttp\\Psr7\\' => 16, |
||||
|
'GuzzleHttp\\Promise\\' => 19, |
||||
|
'GuzzleHttp\\' => 11, |
||||
|
), |
||||
|
'E' => |
||||
|
array ( |
||||
|
'EasyWeChat\\' => 11, |
||||
|
'EasyWeChatComposer\\' => 19, |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
public static $prefixDirsPsr4 = array ( |
||||
|
'Yansongda\\Supports\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/yansongda/supports/src', |
||||
|
), |
||||
|
'Yansongda\\Pay\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/yansongda/pay/src', |
||||
|
), |
||||
|
'Workerman\\MySQL\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/workerman/mysql/src', |
||||
|
), |
||||
|
'Workerman\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/workerman/workerman', |
||||
|
), |
||||
|
'Symfony\\Polyfill\\Php72\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/symfony/polyfill-php72', |
||||
|
), |
||||
|
'Symfony\\Polyfill\\Mbstring\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', |
||||
|
), |
||||
|
'Symfony\\Polyfill\\Intl\\Idn\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn', |
||||
|
), |
||||
|
'Symfony\\Contracts\\Service\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/symfony/service-contracts', |
||||
|
), |
||||
|
'Symfony\\Contracts\\EventDispatcher\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/symfony/event-dispatcher-contracts', |
||||
|
), |
||||
|
'Symfony\\Contracts\\Cache\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/symfony/cache-contracts', |
||||
|
), |
||||
|
'Symfony\\Component\\VarExporter\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/symfony/var-exporter', |
||||
|
), |
||||
|
'Symfony\\Component\\Mime\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/symfony/mime', |
||||
|
), |
||||
|
'Symfony\\Component\\HttpFoundation\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/symfony/http-foundation', |
||||
|
), |
||||
|
'Symfony\\Component\\EventDispatcher\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/symfony/event-dispatcher', |
||||
|
), |
||||
|
'Symfony\\Component\\Cache\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/symfony/cache', |
||||
|
), |
||||
|
'Symfony\\Bridge\\PsrHttpMessage\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/symfony/psr-http-message-bridge', |
||||
|
), |
||||
|
'Qiniu\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/qiniu/php-sdk/src/Qiniu', |
||||
|
), |
||||
|
'Psr\\SimpleCache\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/psr/simple-cache/src', |
||||
|
), |
||||
|
'Psr\\Log\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/psr/log/Psr/Log', |
||||
|
), |
||||
|
'Psr\\Http\\Message\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/psr/http-message/src', |
||||
|
), |
||||
|
'Psr\\Container\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/psr/container/src', |
||||
|
), |
||||
|
'Psr\\Cache\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/psr/cache/src', |
||||
|
), |
||||
|
'Overtrue\\Socialite\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/overtrue/socialite/src', |
||||
|
), |
||||
|
'Monolog\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog', |
||||
|
), |
||||
|
'GuzzleHttp\\Psr7\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src', |
||||
|
), |
||||
|
'GuzzleHttp\\Promise\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/guzzlehttp/promises/src', |
||||
|
), |
||||
|
'GuzzleHttp\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src', |
||||
|
), |
||||
|
'EasyWeChat\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/overtrue/wechat/src', |
||||
|
), |
||||
|
'EasyWeChatComposer\\' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/easywechat-composer/easywechat-composer/src', |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
public static $prefixesPsr0 = array ( |
||||
|
'P' => |
||||
|
array ( |
||||
|
'Pimple' => |
||||
|
array ( |
||||
|
0 => __DIR__ . '/..' . '/pimple/pimple/src', |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
public static function getInitializer(ClassLoader $loader) |
||||
|
{ |
||||
|
return \Closure::bind(function () use ($loader) { |
||||
|
$loader->prefixLengthsPsr4 = ComposerStaticInitabe3555d91d8f0317ed20df0cce34e14::$prefixLengthsPsr4; |
||||
|
$loader->prefixDirsPsr4 = ComposerStaticInitabe3555d91d8f0317ed20df0cce34e14::$prefixDirsPsr4; |
||||
|
$loader->prefixesPsr0 = ComposerStaticInitabe3555d91d8f0317ed20df0cce34e14::$prefixesPsr0; |
||||
|
|
||||
|
}, null, ClassLoader::class); |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,4 @@ |
|||||
|
/vendor |
||||
|
composer.lock |
||||
|
extensions.php |
||||
|
.php_cs.cache |
||||
@ -0,0 +1,29 @@ |
|||||
|
<?php |
||||
|
|
||||
|
$header = <<<EOF |
||||
|
This file is part of the EasyWeChatComposer. |
||||
|
|
||||
|
(c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
|
||||
|
This source file is subject to the MIT license that is bundled |
||||
|
with this source code in the file LICENSE. |
||||
|
EOF; |
||||
|
|
||||
|
return PhpCsFixer\Config::create() |
||||
|
->setRiskyAllowed(true) |
||||
|
->setRules([ |
||||
|
'@Symfony' => true, |
||||
|
'header_comment' => ['header' => $header], |
||||
|
'declare_strict_types' => true, |
||||
|
'ordered_imports' => true, |
||||
|
'strict_comparison' => true, |
||||
|
'no_empty_comment' => false, |
||||
|
'yoda_style' => false, |
||||
|
]) |
||||
|
->setFinder( |
||||
|
PhpCsFixer\Finder::create() |
||||
|
->exclude('vendor') |
||||
|
->notPath('src/Laravel/config.php', 'src/Laravel/routes.php') |
||||
|
->in(__DIR__) |
||||
|
) |
||||
|
; |
||||
@ -0,0 +1,12 @@ |
|||||
|
language: php |
||||
|
|
||||
|
php: |
||||
|
- 7.0 |
||||
|
- 7.1 |
||||
|
- 7.2 |
||||
|
- 7.3 |
||||
|
|
||||
|
install: |
||||
|
- travis_retry composer install --no-interaction --no-suggest |
||||
|
|
||||
|
script: ./vendor/bin/phpunit |
||||
@ -0,0 +1,21 @@ |
|||||
|
MIT License |
||||
|
|
||||
|
Copyright (c) 张铭阳 |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in all |
||||
|
copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
SOFTWARE. |
||||
@ -0,0 +1,55 @@ |
|||||
|
<p align="center"> |
||||
|
<h1 align="center">EasyWeChat Composer Plugin</h1> |
||||
|
</p> |
||||
|
|
||||
|
<p align="center"> |
||||
|
<a href="https://travis-ci.org/mingyoung/easywechat-composer"><img src="https://travis-ci.org/mingyoung/easywechat-composer.svg" alt="Build Status"></a> |
||||
|
<a href="https://scrutinizer-ci.com/g/mingyoung/easywechat-composer/?branch=master"><img src="https://scrutinizer-ci.com/g/mingyoung/easywechat-composer/badges/quality-score.png?b=master" alt="Scrutinizer Code Quality"></a> |
||||
|
<a href="https://packagist.org/packages/easywechat-composer/easywechat-composer"><img src="https://poser.pugx.org/easywechat-composer/easywechat-composer/v/stable.svg" alt="Latest Stable Version"></a> |
||||
|
<a href="https://packagist.org/packages/easywechat-composer/easywechat-composer"><img src="https://poser.pugx.org/easywechat-composer/easywechat-composer/d/total.svg" alt="Total Downloads"></a> |
||||
|
<a href="https://packagist.org/packages/easywechat-composer/easywechat-composer"><img src="https://poser.pugx.org/easywechat-composer/easywechat-composer/license.svg" alt="License"></a> |
||||
|
</p> |
||||
|
|
||||
|
Usage |
||||
|
--- |
||||
|
|
||||
|
Set the `type` to be `easywechat-extension` in your package composer.json file: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"name": "your/package", |
||||
|
"type": "easywechat-extension" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Specify server observer classes in the extra section: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"name": "your/package", |
||||
|
"type": "easywechat-extension", |
||||
|
"extra": { |
||||
|
"observers": [ |
||||
|
"Acme\\Observers\\Handler" |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Examples |
||||
|
--- |
||||
|
* [easywechat-composer/open-platform-testcase](https://github.com/mingyoung/open-platform-testcase) |
||||
|
|
||||
|
Server Delegation |
||||
|
--- |
||||
|
|
||||
|
> 目前仅支持 Laravel |
||||
|
|
||||
|
1. 在 `config/app.php` 中添加 `EasyWeChatComposer\Laravel\ServiceProvider::class` |
||||
|
|
||||
|
2. 在**本地项目**的 `.env` 文件中添加如下配置: |
||||
|
|
||||
|
``` |
||||
|
EASYWECHAT_DELEGATION=true # false 则不启用 |
||||
|
EASYWECHAT_DELEGATION_HOST=https://example.com # 线上域名 |
||||
|
``` |
||||
@ -0,0 +1,35 @@ |
|||||
|
{ |
||||
|
"name": "easywechat-composer/easywechat-composer", |
||||
|
"description": "The composer plugin for EasyWeChat", |
||||
|
"type": "composer-plugin", |
||||
|
"license": "MIT", |
||||
|
"authors": [ |
||||
|
{ |
||||
|
"name": "张铭阳", |
||||
|
"email": "mingyoungcheung@gmail.com" |
||||
|
} |
||||
|
], |
||||
|
"require": { |
||||
|
"php": ">=7.0", |
||||
|
"composer-plugin-api": "^1.0" |
||||
|
}, |
||||
|
"require-dev": { |
||||
|
"composer/composer": "^1.0", |
||||
|
"phpunit/phpunit": "^6.5 || ^7.0" |
||||
|
}, |
||||
|
"autoload": { |
||||
|
"psr-4": { |
||||
|
"EasyWeChatComposer\\": "src/" |
||||
|
} |
||||
|
}, |
||||
|
"autoload-dev": { |
||||
|
"psr-4": { |
||||
|
"EasyWeChatComposer\\Tests\\": "tests/" |
||||
|
} |
||||
|
}, |
||||
|
"extra": { |
||||
|
"class": "EasyWeChatComposer\\Plugin" |
||||
|
}, |
||||
|
"minimum-stability": "dev", |
||||
|
"prefer-stable": true |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/7.0/phpunit.xsd" |
||||
|
bootstrap="vendor/autoload.php" |
||||
|
colors="true" |
||||
|
forceCoversAnnotation="true" |
||||
|
beStrictAboutCoversAnnotation="true" |
||||
|
beStrictAboutOutputDuringTests="true" |
||||
|
beStrictAboutTodoAnnotatedTests="true" |
||||
|
verbose="true"> |
||||
|
<testsuite name="EasyWeChatComposer Test"> |
||||
|
<directory suffix="Test.php">tests</directory> |
||||
|
</testsuite> |
||||
|
|
||||
|
<filter> |
||||
|
<whitelist processUncoveredFilesFromWhitelist="true"> |
||||
|
<directory suffix=".php">src</directory> |
||||
|
</whitelist> |
||||
|
</filter> |
||||
|
</phpunit> |
||||
@ -0,0 +1,63 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Commands; |
||||
|
|
||||
|
use Composer\Command\BaseCommand; |
||||
|
use Symfony\Component\Console\Helper\Table; |
||||
|
use Symfony\Component\Console\Input\InputInterface; |
||||
|
use Symfony\Component\Console\Output\OutputInterface; |
||||
|
|
||||
|
class ExtensionsCommand extends BaseCommand |
||||
|
{ |
||||
|
/** |
||||
|
* Configures the current command. |
||||
|
*/ |
||||
|
protected function configure() |
||||
|
{ |
||||
|
$this->setName('easywechat:extensions') |
||||
|
->setDescription('Lists all installed extensions.'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Executes the current command. |
||||
|
* |
||||
|
* @param InputInterface $input |
||||
|
* @param OutputInterface $output |
||||
|
*/ |
||||
|
protected function execute(InputInterface $input, OutputInterface $output) |
||||
|
{ |
||||
|
$extensions = require __DIR__.'/../../extensions.php'; |
||||
|
|
||||
|
if (empty($extensions) || !is_array($extensions)) { |
||||
|
return $output->writeln('<info>No extension installed.</info>'); |
||||
|
} |
||||
|
|
||||
|
$table = new Table($output); |
||||
|
$table->setHeaders(['Name', 'Observers']) |
||||
|
->setRows( |
||||
|
array_map([$this, 'getRows'], array_keys($extensions), $extensions) |
||||
|
)->render(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param string $name |
||||
|
* @param array $extension |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
protected function getRows($name, $extension) |
||||
|
{ |
||||
|
return [$name, implode("\n", $extension['observers'] ?? [])]; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Commands; |
||||
|
|
||||
|
use Composer\Plugin\Capability\CommandProvider; |
||||
|
|
||||
|
class Provider implements CommandProvider |
||||
|
{ |
||||
|
/** |
||||
|
* Retrieves an array of commands. |
||||
|
* |
||||
|
* @return \Composer\Command\BaseCommand[] |
||||
|
*/ |
||||
|
public function getCommands() |
||||
|
{ |
||||
|
return [ |
||||
|
new ExtensionsCommand(), |
||||
|
]; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Contracts; |
||||
|
|
||||
|
interface Encrypter |
||||
|
{ |
||||
|
/** |
||||
|
* Encrypt the given value. |
||||
|
* |
||||
|
* @param string $value |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function encrypt($value); |
||||
|
|
||||
|
/** |
||||
|
* Decrypt the given value. |
||||
|
* |
||||
|
* @param string $payload |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function decrypt($payload); |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Delegation; |
||||
|
|
||||
|
use EasyWeChatComposer\EasyWeChat; |
||||
|
|
||||
|
class DelegationOptions |
||||
|
{ |
||||
|
/** |
||||
|
* @var array |
||||
|
*/ |
||||
|
protected $config = [ |
||||
|
'enabled' => false, |
||||
|
]; |
||||
|
|
||||
|
/** |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function enable() |
||||
|
{ |
||||
|
$this->config['enabled'] = true; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function disable() |
||||
|
{ |
||||
|
$this->config['enabled'] = false; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param bool $ability |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function ability($ability) |
||||
|
{ |
||||
|
$this->config['enabled'] = (bool) $ability; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param string $host |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function toHost($host) |
||||
|
{ |
||||
|
$this->config['host'] = $host; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Destructor. |
||||
|
*/ |
||||
|
public function __destruct() |
||||
|
{ |
||||
|
EasyWeChat::mergeConfig([ |
||||
|
'delegation' => $this->config, |
||||
|
]); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,83 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Delegation; |
||||
|
|
||||
|
use EasyWeChatComposer\Traits\MakesHttpRequests; |
||||
|
|
||||
|
class DelegationTo |
||||
|
{ |
||||
|
use MakesHttpRequests; |
||||
|
|
||||
|
/** |
||||
|
* @var \EasyWeChat\Kernel\ServiceContainer |
||||
|
*/ |
||||
|
protected $app; |
||||
|
|
||||
|
/** |
||||
|
* @var array |
||||
|
*/ |
||||
|
protected $identifiers = []; |
||||
|
|
||||
|
/** |
||||
|
* @param \EasyWeChat\Kernel\ServiceContainer $app |
||||
|
* @param string $identifier |
||||
|
*/ |
||||
|
public function __construct($app, $identifier) |
||||
|
{ |
||||
|
$this->app = $app; |
||||
|
|
||||
|
$this->push($identifier); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param string $identifier |
||||
|
*/ |
||||
|
public function push($identifier) |
||||
|
{ |
||||
|
$this->identifiers[] = $identifier; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param string $identifier |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function __get($identifier) |
||||
|
{ |
||||
|
$this->push($identifier); |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param string $method |
||||
|
* @param array $arguments |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function __call($method, $arguments) |
||||
|
{ |
||||
|
$config = array_intersect_key($this->app->getConfig(), array_flip(['app_id', 'secret', 'token', 'aes_key', 'response_type', 'component_app_id', 'refresh_token'])); |
||||
|
|
||||
|
$data = [ |
||||
|
'config' => $config, |
||||
|
'application' => get_class($this->app), |
||||
|
'identifiers' => $this->identifiers, |
||||
|
'method' => $method, |
||||
|
'arguments' => $arguments, |
||||
|
]; |
||||
|
|
||||
|
return $this->request('easywechat-composer/delegate', $data); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,83 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Delegation; |
||||
|
|
||||
|
use EasyWeChat; |
||||
|
use EasyWeChatComposer\Http\DelegationResponse; |
||||
|
|
||||
|
class Hydrate |
||||
|
{ |
||||
|
/** |
||||
|
* @var array |
||||
|
*/ |
||||
|
protected $attributes; |
||||
|
|
||||
|
/** |
||||
|
* @param array $attributes |
||||
|
*/ |
||||
|
public function __construct(array $attributes) |
||||
|
{ |
||||
|
$this->attributes = $attributes; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function handle() |
||||
|
{ |
||||
|
$app = $this->createsApplication()->shouldntDelegate(); |
||||
|
|
||||
|
foreach ($this->attributes['identifiers'] as $identifier) { |
||||
|
$app = $app->$identifier; |
||||
|
} |
||||
|
|
||||
|
return call_user_func_array([$app, $this->attributes['method']], $this->attributes['arguments']); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return \EasyWeChat\Kernel\ServiceContainer |
||||
|
*/ |
||||
|
protected function createsApplication() |
||||
|
{ |
||||
|
$application = $this->attributes['application']; |
||||
|
|
||||
|
if ($application === EasyWeChat\OpenPlatform\Authorizer\OfficialAccount\Application::class) { |
||||
|
return $this->createsOpenPlatformApplication('officialAccount'); |
||||
|
} |
||||
|
|
||||
|
if ($application === EasyWeChat\OpenPlatform\Authorizer\MiniProgram\Application::class) { |
||||
|
return $this->createsOpenPlatformApplication('miniProgram'); |
||||
|
} |
||||
|
|
||||
|
return new $application($this->buildConfig($this->attributes['config'])); |
||||
|
} |
||||
|
|
||||
|
protected function createsOpenPlatformApplication($type) |
||||
|
{ |
||||
|
$config = $this->attributes['config']; |
||||
|
|
||||
|
$authorizerAppId = $config['app_id']; |
||||
|
|
||||
|
$config['app_id'] = $config['component_app_id']; |
||||
|
|
||||
|
return EasyWeChat\Factory::openPlatform($this->buildConfig($config))->$type($authorizerAppId, $config['refresh_token']); |
||||
|
} |
||||
|
|
||||
|
protected function buildConfig(array $config) |
||||
|
{ |
||||
|
$config['response_type'] = DelegationResponse::class; |
||||
|
|
||||
|
return $config; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer; |
||||
|
|
||||
|
use EasyWeChatComposer\Delegation\DelegationOptions; |
||||
|
|
||||
|
class EasyWeChat |
||||
|
{ |
||||
|
/** |
||||
|
* @var array |
||||
|
*/ |
||||
|
protected static $config = []; |
||||
|
|
||||
|
/** |
||||
|
* Encryption key. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected static $encryptionKey; |
||||
|
|
||||
|
/** |
||||
|
* @param array $config |
||||
|
*/ |
||||
|
public static function mergeConfig(array $config) |
||||
|
{ |
||||
|
static::$config = array_merge(static::$config, $config); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return array |
||||
|
*/ |
||||
|
public static function config() |
||||
|
{ |
||||
|
return static::$config; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set encryption key. |
||||
|
* |
||||
|
* @param string $key |
||||
|
* |
||||
|
* @return static |
||||
|
*/ |
||||
|
public static function setEncryptionKey(string $key) |
||||
|
{ |
||||
|
static::$encryptionKey = $key; |
||||
|
|
||||
|
return new static(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get encryption key. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public static function getEncryptionKey(): string |
||||
|
{ |
||||
|
return static::$encryptionKey; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return \EasyWeChatComposer\Delegation\DelegationOptions |
||||
|
*/ |
||||
|
public static function withDelegation() |
||||
|
{ |
||||
|
return new DelegationOptions(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Encryption; |
||||
|
|
||||
|
use EasyWeChatComposer\Contracts\Encrypter; |
||||
|
use EasyWeChatComposer\Exceptions\DecryptException; |
||||
|
use EasyWeChatComposer\Exceptions\EncryptException; |
||||
|
|
||||
|
class DefaultEncrypter implements Encrypter |
||||
|
{ |
||||
|
/** |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $key; |
||||
|
|
||||
|
/** |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $cipher; |
||||
|
|
||||
|
/** |
||||
|
* @param string $key |
||||
|
* @param string $cipher |
||||
|
*/ |
||||
|
public function __construct($key, $cipher = 'AES-256-CBC') |
||||
|
{ |
||||
|
$this->key = $key; |
||||
|
$this->cipher = $cipher; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Encrypt the given value. |
||||
|
* |
||||
|
* @param string $value |
||||
|
* |
||||
|
* @return string |
||||
|
* |
||||
|
* @throws \EasyWeChatComposer\Exceptions\EncryptException |
||||
|
*/ |
||||
|
public function encrypt($value) |
||||
|
{ |
||||
|
$iv = random_bytes(openssl_cipher_iv_length($this->cipher)); |
||||
|
|
||||
|
$value = openssl_encrypt($value, $this->cipher, $this->key, 0, $iv); |
||||
|
|
||||
|
if ($value === false) { |
||||
|
throw new EncryptException('Could not encrypt the data.'); |
||||
|
} |
||||
|
|
||||
|
$iv = base64_encode($iv); |
||||
|
|
||||
|
return base64_encode(json_encode(compact('iv', 'value'))); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Decrypt the given value. |
||||
|
* |
||||
|
* @param string $payload |
||||
|
* |
||||
|
* @return string |
||||
|
* |
||||
|
* @throws \EasyWeChatComposer\Exceptions\DecryptException |
||||
|
*/ |
||||
|
public function decrypt($payload) |
||||
|
{ |
||||
|
$payload = json_decode(base64_decode($payload), true); |
||||
|
|
||||
|
$iv = base64_decode($payload['iv']); |
||||
|
|
||||
|
$decrypted = openssl_decrypt($payload['value'], $this->cipher, $this->key, 0, $iv); |
||||
|
|
||||
|
if ($decrypted === false) { |
||||
|
throw new DecryptException('Could not decrypt the data.'); |
||||
|
} |
||||
|
|
||||
|
return $decrypted; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Exceptions; |
||||
|
|
||||
|
use Exception; |
||||
|
|
||||
|
class DecryptException extends Exception |
||||
|
{ |
||||
|
// |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Exceptions; |
||||
|
|
||||
|
use Exception; |
||||
|
|
||||
|
class DelegationException extends Exception |
||||
|
{ |
||||
|
/** |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $exception; |
||||
|
|
||||
|
/** |
||||
|
* @param string $exception |
||||
|
*/ |
||||
|
public function setException($exception) |
||||
|
{ |
||||
|
$this->exception = $exception; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getException() |
||||
|
{ |
||||
|
return $this->exception; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Exceptions; |
||||
|
|
||||
|
use Exception; |
||||
|
|
||||
|
class EncryptException extends Exception |
||||
|
{ |
||||
|
// |
||||
|
} |
||||
@ -0,0 +1,143 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer; |
||||
|
|
||||
|
use EasyWeChat\Kernel\Contracts\EventHandlerInterface; |
||||
|
use EasyWeChat\Kernel\ServiceContainer; |
||||
|
use ReflectionClass; |
||||
|
|
||||
|
class Extension |
||||
|
{ |
||||
|
/** |
||||
|
* @var \EasyWeChat\Kernel\ServiceContainer |
||||
|
*/ |
||||
|
protected $app; |
||||
|
|
||||
|
/** |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $manifestPath; |
||||
|
|
||||
|
/** |
||||
|
* @var array|null |
||||
|
*/ |
||||
|
protected $manifest; |
||||
|
|
||||
|
/** |
||||
|
* @param \EasyWeChat\Kernel\ServiceContainer $app |
||||
|
*/ |
||||
|
public function __construct(ServiceContainer $app) |
||||
|
{ |
||||
|
$this->app = $app; |
||||
|
$this->manifestPath = __DIR__.'/../extensions.php'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get observers. |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function observers(): array |
||||
|
{ |
||||
|
if ($this->shouldIgnore()) { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
$observers = []; |
||||
|
|
||||
|
foreach ($this->getManifest() as $name => $extra) { |
||||
|
$observers = array_merge($observers, $extra['observers'] ?? []); |
||||
|
} |
||||
|
|
||||
|
return array_map([$this, 'listObserver'], array_filter($observers, [$this, 'validateObserver'])); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param mixed $observer |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
protected function isDisable($observer): bool |
||||
|
{ |
||||
|
return in_array($observer, $this->app->config->get('disable_observers', [])); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the observers should be ignore. |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
protected function shouldIgnore(): bool |
||||
|
{ |
||||
|
return !file_exists($this->manifestPath) || $this->isDisable('*'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Validate the given observer. |
||||
|
* |
||||
|
* @param mixed $observer |
||||
|
* |
||||
|
* @return bool |
||||
|
* |
||||
|
* @throws \ReflectionException |
||||
|
*/ |
||||
|
protected function validateObserver($observer): bool |
||||
|
{ |
||||
|
return !$this->isDisable($observer) |
||||
|
&& (new ReflectionClass($observer))->implementsInterface(EventHandlerInterface::class) |
||||
|
&& $this->accessible($observer); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Determine whether the given observer is accessible. |
||||
|
* |
||||
|
* @param string $observer |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
protected function accessible($observer): bool |
||||
|
{ |
||||
|
if (!method_exists($observer, 'getAccessor')) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return in_array(get_class($this->app), (array) $observer::getAccessor()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param mixed $observer |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
protected function listObserver($observer): array |
||||
|
{ |
||||
|
$condition = method_exists($observer, 'onCondition') ? $observer::onCondition() : '*'; |
||||
|
|
||||
|
return [$observer, $condition]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the easywechat manifest. |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
protected function getManifest(): array |
||||
|
{ |
||||
|
if (!is_null($this->manifest)) { |
||||
|
return $this->manifest; |
||||
|
} |
||||
|
|
||||
|
return $this->manifest = file_exists($this->manifestPath) ? require $this->manifestPath : []; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Http; |
||||
|
|
||||
|
class DelegationResponse extends Response |
||||
|
{ |
||||
|
/** |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getBodyContents() |
||||
|
{ |
||||
|
return $this->response->getBodyContents(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,104 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Http; |
||||
|
|
||||
|
use EasyWeChat\Kernel\Contracts\Arrayable; |
||||
|
use EasyWeChat\Kernel\Http\Response as HttpResponse; |
||||
|
use JsonSerializable; |
||||
|
|
||||
|
class Response implements Arrayable, JsonSerializable |
||||
|
{ |
||||
|
/** |
||||
|
* @var \EasyWeChat\Kernel\Http\Response |
||||
|
*/ |
||||
|
protected $response; |
||||
|
|
||||
|
/** |
||||
|
* @var array |
||||
|
*/ |
||||
|
protected $array; |
||||
|
|
||||
|
/** |
||||
|
* @param \EasyWeChat\Kernel\Http\Response $response |
||||
|
*/ |
||||
|
public function __construct(HttpResponse $response) |
||||
|
{ |
||||
|
$this->response = $response; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @see \ArrayAccess::offsetExists |
||||
|
* |
||||
|
* @param string $offset |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function offsetExists($offset) |
||||
|
{ |
||||
|
return isset($this->toArray()[$offset]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @see \ArrayAccess::offsetGet |
||||
|
* |
||||
|
* @param string $offset |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function offsetGet($offset) |
||||
|
{ |
||||
|
return $this->toArray()[$offset] ?? null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @see \ArrayAccess::offsetSet |
||||
|
* |
||||
|
* @param string $offset |
||||
|
* @param mixed $value |
||||
|
*/ |
||||
|
public function offsetSet($offset, $value) |
||||
|
{ |
||||
|
// |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @see \ArrayAccess::offsetUnset |
||||
|
* |
||||
|
* @param string $offset |
||||
|
*/ |
||||
|
public function offsetUnset($offset) |
||||
|
{ |
||||
|
// |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the instance as an array. |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function toArray() |
||||
|
{ |
||||
|
return $this->array ?: $this->array = $this->response->toArray(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert the object into something JSON serializable. |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function jsonSerialize() |
||||
|
{ |
||||
|
return $this->toArray(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Laravel\Http\Controllers; |
||||
|
|
||||
|
use EasyWeChatComposer\Delegation\Hydrate; |
||||
|
use EasyWeChatComposer\Encryption\DefaultEncrypter; |
||||
|
use Illuminate\Http\Request; |
||||
|
use Throwable; |
||||
|
|
||||
|
class DelegatesController |
||||
|
{ |
||||
|
/** |
||||
|
* @param \Illuminate\Http\Request $request |
||||
|
* @param \EasyWeChatComposer\Encryption\DefaultEncrypter $encrypter |
||||
|
* |
||||
|
* @return \Illuminate\Http\Response |
||||
|
*/ |
||||
|
public function __invoke(Request $request, DefaultEncrypter $encrypter) |
||||
|
{ |
||||
|
try { |
||||
|
$data = json_decode($encrypter->decrypt($request->get('encrypted')), true); |
||||
|
|
||||
|
$hydrate = new Hydrate($data); |
||||
|
|
||||
|
$response = $hydrate->handle(); |
||||
|
|
||||
|
return response()->json([ |
||||
|
'response_type' => get_class($response), |
||||
|
'response' => $encrypter->encrypt($response->getBodyContents()), |
||||
|
]); |
||||
|
} catch (Throwable $t) { |
||||
|
return [ |
||||
|
'exception' => get_class($t), |
||||
|
'message' => $t->getMessage(), |
||||
|
]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,112 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Laravel; |
||||
|
|
||||
|
use EasyWeChatComposer\EasyWeChat; |
||||
|
use EasyWeChatComposer\Encryption\DefaultEncrypter; |
||||
|
use Illuminate\Support\Facades\Cache; |
||||
|
use Illuminate\Support\Facades\Route; |
||||
|
use Illuminate\Support\ServiceProvider as LaravelServiceProvider; |
||||
|
use RuntimeException; |
||||
|
|
||||
|
class ServiceProvider extends LaravelServiceProvider |
||||
|
{ |
||||
|
/** |
||||
|
* Bootstrap any application services. |
||||
|
*/ |
||||
|
public function boot() |
||||
|
{ |
||||
|
$this->registerRoutes(); |
||||
|
$this->publishes([ |
||||
|
__DIR__.'/config.php' => config_path('easywechat-composer.php'), |
||||
|
]); |
||||
|
|
||||
|
EasyWeChat::setEncryptionKey( |
||||
|
$defaultKey = $this->getKey() |
||||
|
); |
||||
|
|
||||
|
EasyWeChat::withDelegation() |
||||
|
->toHost($this->config('delegation.host')) |
||||
|
->ability($this->config('delegation.enabled')); |
||||
|
|
||||
|
$this->app->when(DefaultEncrypter::class)->needs('$key')->give($defaultKey); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Register routes. |
||||
|
*/ |
||||
|
protected function registerRoutes() |
||||
|
{ |
||||
|
Route::prefix('easywechat-composer')->namespace('EasyWeChatComposer\Laravel\Http\Controllers')->group(function () { |
||||
|
$this->loadRoutesFrom(__DIR__.'/routes.php'); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Register any application services. |
||||
|
*/ |
||||
|
public function register() |
||||
|
{ |
||||
|
$this->configure(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Register config. |
||||
|
*/ |
||||
|
protected function configure() |
||||
|
{ |
||||
|
$this->mergeConfigFrom( |
||||
|
__DIR__.'/config.php', 'easywechat-composer' |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the specified configuration value. |
||||
|
* |
||||
|
* @param string|null $key |
||||
|
* @param mixed $default |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
protected function config($key = null, $default = null) |
||||
|
{ |
||||
|
$config = $this->app['config']->get('easywechat-composer'); |
||||
|
|
||||
|
if (is_null($key)) { |
||||
|
return $config; |
||||
|
} |
||||
|
|
||||
|
return array_get($config, $key, $default); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return string |
||||
|
*/ |
||||
|
protected function getKey() |
||||
|
{ |
||||
|
return $this->config('encryption.key') ?: $this->getMd5Key(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return string |
||||
|
*/ |
||||
|
protected function getMd5Key() |
||||
|
{ |
||||
|
return Cache::remember('easywechat-composer.encryption_key', 30, function () { |
||||
|
throw_unless(file_exists($path = base_path('composer.lock')), RuntimeException::class, 'No encryption key provided.'); |
||||
|
|
||||
|
return md5_file($path); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) mingyoung <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
return [ |
||||
|
|
||||
|
'encryption' => [ |
||||
|
|
||||
|
'key' => env('EASYWECHAT_KEY'), |
||||
|
|
||||
|
], |
||||
|
|
||||
|
'delegation' => [ |
||||
|
|
||||
|
'enabled' => env('EASYWECHAT_DELEGATION', false), |
||||
|
|
||||
|
'host' => env('EASYWECHAT_DELEGATION_HOST'), |
||||
|
], |
||||
|
|
||||
|
]; |
||||
@ -0,0 +1,16 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
use Illuminate\Support\Facades\Route; |
||||
|
|
||||
|
Route::post('delegate', 'DelegatesController'); |
||||
@ -0,0 +1,120 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer; |
||||
|
|
||||
|
class ManifestManager |
||||
|
{ |
||||
|
const PACKAGE_TYPE = 'easywechat-extension'; |
||||
|
|
||||
|
const EXTRA_OBSERVER = 'observers'; |
||||
|
|
||||
|
/** |
||||
|
* The vendor path. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $vendorPath; |
||||
|
|
||||
|
/** |
||||
|
* The manifest path. |
||||
|
* |
||||
|
* @var string |
||||
|
*/ |
||||
|
protected $manifestPath; |
||||
|
|
||||
|
/** |
||||
|
* @param string $vendorPath |
||||
|
* @param string|null $manifestPath |
||||
|
*/ |
||||
|
public function __construct(string $vendorPath, string $manifestPath = null) |
||||
|
{ |
||||
|
$this->vendorPath = $vendorPath; |
||||
|
$this->manifestPath = $manifestPath ?: $vendorPath.'/easywechat-composer/easywechat-composer/extensions.php'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Remove manifest file. |
||||
|
* |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function unlink() |
||||
|
{ |
||||
|
if (file_exists($this->manifestPath)) { |
||||
|
@unlink($this->manifestPath); |
||||
|
} |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Build the manifest file. |
||||
|
*/ |
||||
|
public function build() |
||||
|
{ |
||||
|
$packages = []; |
||||
|
|
||||
|
if (file_exists($installed = $this->vendorPath.'/composer/installed.json')) { |
||||
|
$packages = json_decode(file_get_contents($installed), true); |
||||
|
} |
||||
|
|
||||
|
$this->write($this->map($packages)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param array $packages |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
protected function map(array $packages): array |
||||
|
{ |
||||
|
$manifest = []; |
||||
|
|
||||
|
$packages = array_filter($packages, function ($package) { |
||||
|
return $package['type'] === self::PACKAGE_TYPE; |
||||
|
}); |
||||
|
|
||||
|
foreach ($packages as $package) { |
||||
|
$manifest[$package['name']] = [self::EXTRA_OBSERVER => $package['extra'][self::EXTRA_OBSERVER] ?? []]; |
||||
|
} |
||||
|
|
||||
|
return $manifest; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Write the manifest array to a file. |
||||
|
* |
||||
|
* @param array $manifest |
||||
|
*/ |
||||
|
protected function write(array $manifest) |
||||
|
{ |
||||
|
file_put_contents( |
||||
|
$this->manifestPath, |
||||
|
'<?php return '.var_export($manifest, true).';' |
||||
|
); |
||||
|
|
||||
|
$this->invalidate($this->manifestPath); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Invalidate the given file. |
||||
|
* |
||||
|
* @param string $file |
||||
|
*/ |
||||
|
protected function invalidate($file) |
||||
|
{ |
||||
|
if (function_exists('opcache_invalidate')) { |
||||
|
@opcache_invalidate($file, true); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer; |
||||
|
|
||||
|
use Composer\Composer; |
||||
|
use Composer\EventDispatcher\EventSubscriberInterface; |
||||
|
use Composer\Installer\PackageEvent; |
||||
|
use Composer\Installer\PackageEvents; |
||||
|
use Composer\IO\IOInterface; |
||||
|
use Composer\Plugin\Capable; |
||||
|
use Composer\Plugin\PluginInterface; |
||||
|
use Composer\Script\Event; |
||||
|
use Composer\Script\ScriptEvents; |
||||
|
|
||||
|
class Plugin implements PluginInterface, EventSubscriberInterface, Capable |
||||
|
{ |
||||
|
/** |
||||
|
* @var bool |
||||
|
*/ |
||||
|
protected $activated = true; |
||||
|
|
||||
|
/** |
||||
|
* Apply plugin modifications to Composer. |
||||
|
* |
||||
|
* @param \Composer\Composer $composer |
||||
|
* @param \Composer\IO\IOInterface $io |
||||
|
*/ |
||||
|
public function activate(Composer $composer, IOInterface $io) |
||||
|
{ |
||||
|
// |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function getCapabilities() |
||||
|
{ |
||||
|
return [ |
||||
|
'Composer\Plugin\Capability\CommandProvider' => 'EasyWeChatComposer\Commands\Provider', |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Listen events. |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public static function getSubscribedEvents() |
||||
|
{ |
||||
|
return [ |
||||
|
PackageEvents::PRE_PACKAGE_UNINSTALL => 'prePackageUninstall', |
||||
|
ScriptEvents::POST_AUTOLOAD_DUMP => 'postAutoloadDump', |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param \Composer\Installer\PackageEvent |
||||
|
*/ |
||||
|
public function prePackageUninstall(PackageEvent $event) |
||||
|
{ |
||||
|
if ($event->getOperation()->getPackage()->getName() === 'overtrue/wechat') { |
||||
|
$this->activated = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param \Composer\Script\Event $event |
||||
|
*/ |
||||
|
public function postAutoloadDump(Event $event) |
||||
|
{ |
||||
|
if (!$this->activated) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
$manifest = new ManifestManager( |
||||
|
rtrim($event->getComposer()->getConfig()->get('vendor-dir'), '/') |
||||
|
); |
||||
|
|
||||
|
$manifest->unlink()->build(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,110 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Traits; |
||||
|
|
||||
|
use EasyWeChat\Kernel\Http\StreamResponse; |
||||
|
use EasyWeChat\Kernel\Traits\ResponseCastable; |
||||
|
use EasyWeChatComposer\Contracts\Encrypter; |
||||
|
use EasyWeChatComposer\EasyWeChat; |
||||
|
use EasyWeChatComposer\Encryption\DefaultEncrypter; |
||||
|
use EasyWeChatComposer\Exceptions\DelegationException; |
||||
|
use GuzzleHttp\Client; |
||||
|
use GuzzleHttp\ClientInterface; |
||||
|
|
||||
|
trait MakesHttpRequests |
||||
|
{ |
||||
|
use ResponseCastable; |
||||
|
|
||||
|
/** |
||||
|
* @var \GuzzleHttp\ClientInterface |
||||
|
*/ |
||||
|
protected $httpClient; |
||||
|
|
||||
|
/** |
||||
|
* @var \EasyWeChatComposer\Contracts\Encrypter |
||||
|
*/ |
||||
|
protected $encrypter; |
||||
|
|
||||
|
/** |
||||
|
* @param string $endpoint |
||||
|
* @param array $payload |
||||
|
* |
||||
|
* @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string |
||||
|
* |
||||
|
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException |
||||
|
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException |
||||
|
* @throws \GuzzleHttp\Exception\GuzzleException |
||||
|
*/ |
||||
|
protected function request($endpoint, array $payload) |
||||
|
{ |
||||
|
$response = $this->getHttpClient()->request('POST', $endpoint, [ |
||||
|
'form_params' => $this->buildFormParams($payload), |
||||
|
]); |
||||
|
|
||||
|
$parsed = $this->parseResponse($response); |
||||
|
|
||||
|
return $this->detectAndCastResponseToType( |
||||
|
$this->getEncrypter()->decrypt($parsed['response']), |
||||
|
($parsed['response_type'] === StreamResponse::class) ? 'raw' : $this->app['config']['response_type'] |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param array $payload |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
protected function buildFormParams($payload) |
||||
|
{ |
||||
|
return [ |
||||
|
'encrypted' => $this->getEncrypter()->encrypt(json_encode($payload)), |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param \Psr\Http\Message\ResponseInterface $response |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
protected function parseResponse($response) |
||||
|
{ |
||||
|
$result = json_decode((string) $response->getBody(), true); |
||||
|
|
||||
|
if (isset($result['exception'])) { |
||||
|
throw (new DelegationException($result['message']))->setException($result['exception']); |
||||
|
} |
||||
|
|
||||
|
return $result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return \GuzzleHttp\ClientInterface |
||||
|
*/ |
||||
|
protected function getHttpClient(): ClientInterface |
||||
|
{ |
||||
|
return $this->httpClient ?: $this->httpClient = new Client([ |
||||
|
'base_uri' => $this->app['config']['delegation']['host'], |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return \EasyWeChatComposer\Contracts\Encrypter |
||||
|
*/ |
||||
|
protected function getEncrypter(): Encrypter |
||||
|
{ |
||||
|
return $this->encrypter ?: $this->encrypter = new DefaultEncrypter( |
||||
|
EasyWeChat::getEncryptionKey() |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Traits; |
||||
|
|
||||
|
use EasyWeChat\Kernel\BaseClient; |
||||
|
use EasyWeChatComposer\Delegation\DelegationTo; |
||||
|
use EasyWeChatComposer\EasyWeChat; |
||||
|
|
||||
|
trait WithAggregator |
||||
|
{ |
||||
|
/** |
||||
|
* Aggregate. |
||||
|
*/ |
||||
|
protected function aggregate() |
||||
|
{ |
||||
|
foreach (EasyWeChat::config() as $key => $value) { |
||||
|
$this['config']->set($key, $value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function shouldDelegate($id) |
||||
|
{ |
||||
|
return $this['config']->get('delegation.enabled') |
||||
|
&& $this->offsetGet($id) instanceof BaseClient; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function shouldntDelegate() |
||||
|
{ |
||||
|
$this['config']->set('delegation.enabled', false); |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param string $id |
||||
|
* |
||||
|
* @return \EasyWeChatComposer\Delegation |
||||
|
*/ |
||||
|
public function delegateTo($id) |
||||
|
{ |
||||
|
return new DelegationTo($this, $id); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
/* |
||||
|
* This file is part of the EasyWeChatComposer. |
||||
|
* |
||||
|
* (c) 张铭阳 <mingyoungcheung@gmail.com> |
||||
|
* |
||||
|
* This source file is subject to the MIT license that is bundled |
||||
|
* with this source code in the file LICENSE. |
||||
|
*/ |
||||
|
|
||||
|
namespace EasyWeChatComposer\Tests; |
||||
|
|
||||
|
use EasyWeChatComposer\ManifestManager; |
||||
|
use PHPUnit\Framework\TestCase; |
||||
|
|
||||
|
class ManifestManagerTest extends TestCase |
||||
|
{ |
||||
|
private $vendorPath; |
||||
|
private $manifestPath; |
||||
|
|
||||
|
protected function getManifestManager() |
||||
|
{ |
||||
|
return new ManifestManager( |
||||
|
$this->vendorPath = __DIR__.'/__fixtures__/vendor/', |
||||
|
$this->manifestPath = __DIR__.'/__fixtures__/extensions.php' |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public function testUnlink() |
||||
|
{ |
||||
|
$this->assertInstanceOf(ManifestManager::class, $this->getManifestManager()->unlink()); |
||||
|
$this->assertFalse(file_exists($this->manifestPath)); |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,19 @@ |
|||||
|
Copyright (c) 2011-2018 Michael Dowling, https://github.com/mtdowling <mtdowling@gmail.com> |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in |
||||
|
all copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
|
THE SOFTWARE. |
||||
@ -0,0 +1,91 @@ |
|||||
|
Guzzle, PHP HTTP client |
||||
|
======================= |
||||
|
|
||||
|
[](https://github.com/guzzle/guzzle/releases) |
||||
|
[](https://travis-ci.org/guzzle/guzzle) |
||||
|
[](https://packagist.org/packages/guzzlehttp/guzzle) |
||||
|
|
||||
|
Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and |
||||
|
trivial to integrate with web services. |
||||
|
|
||||
|
- Simple interface for building query strings, POST requests, streaming large |
||||
|
uploads, streaming large downloads, using HTTP cookies, uploading JSON data, |
||||
|
etc... |
||||
|
- Can send both synchronous and asynchronous requests using the same interface. |
||||
|
- Uses PSR-7 interfaces for requests, responses, and streams. This allows you |
||||
|
to utilize other PSR-7 compatible libraries with Guzzle. |
||||
|
- Abstracts away the underlying HTTP transport, allowing you to write |
||||
|
environment and transport agnostic code; i.e., no hard dependency on cURL, |
||||
|
PHP streams, sockets, or non-blocking event loops. |
||||
|
- Middleware system allows you to augment and compose client behavior. |
||||
|
|
||||
|
```php |
||||
|
$client = new \GuzzleHttp\Client(); |
||||
|
$res = $client->request('GET', 'https://api.github.com/repos/guzzle/guzzle'); |
||||
|
echo $res->getStatusCode(); |
||||
|
// 200 |
||||
|
echo $res->getHeaderLine('content-type'); |
||||
|
// 'application/json; charset=utf8' |
||||
|
echo $res->getBody(); |
||||
|
// '{"id": 1420053, "name": "guzzle", ...}' |
||||
|
|
||||
|
// Send an asynchronous request. |
||||
|
$request = new \GuzzleHttp\Psr7\Request('GET', 'http://httpbin.org'); |
||||
|
$promise = $client->sendAsync($request)->then(function ($response) { |
||||
|
echo 'I completed! ' . $response->getBody(); |
||||
|
}); |
||||
|
$promise->wait(); |
||||
|
``` |
||||
|
|
||||
|
## Help and docs |
||||
|
|
||||
|
- [Documentation](http://guzzlephp.org/) |
||||
|
- [Stack Overflow](http://stackoverflow.com/questions/tagged/guzzle) |
||||
|
- [Gitter](https://gitter.im/guzzle/guzzle) |
||||
|
|
||||
|
|
||||
|
## Installing Guzzle |
||||
|
|
||||
|
The recommended way to install Guzzle is through |
||||
|
[Composer](http://getcomposer.org). |
||||
|
|
||||
|
```bash |
||||
|
# Install Composer |
||||
|
curl -sS https://getcomposer.org/installer | php |
||||
|
``` |
||||
|
|
||||
|
Next, run the Composer command to install the latest stable version of Guzzle: |
||||
|
|
||||
|
```bash |
||||
|
php composer.phar require guzzlehttp/guzzle |
||||
|
``` |
||||
|
|
||||
|
After installing, you need to require Composer's autoloader: |
||||
|
|
||||
|
```php |
||||
|
require 'vendor/autoload.php'; |
||||
|
``` |
||||
|
|
||||
|
You can then later update Guzzle using composer: |
||||
|
|
||||
|
```bash |
||||
|
composer.phar update |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
## Version Guidance |
||||
|
|
||||
|
| Version | Status | Packagist | Namespace | Repo | Docs | PSR-7 | PHP Version | |
||||
|
|---------|------------|---------------------|--------------|---------------------|---------------------|-------|-------------| |
||||
|
| 3.x | EOL | `guzzle/guzzle` | `Guzzle` | [v3][guzzle-3-repo] | [v3][guzzle-3-docs] | No | >= 5.3.3 | |
||||
|
| 4.x | EOL | `guzzlehttp/guzzle` | `GuzzleHttp` | [v4][guzzle-4-repo] | N/A | No | >= 5.4 | |
||||
|
| 5.x | Maintained | `guzzlehttp/guzzle` | `GuzzleHttp` | [v5][guzzle-5-repo] | [v5][guzzle-5-docs] | No | >= 5.4 | |
||||
|
| 6.x | Latest | `guzzlehttp/guzzle` | `GuzzleHttp` | [v6][guzzle-6-repo] | [v6][guzzle-6-docs] | Yes | >= 5.5 | |
||||
|
|
||||
|
[guzzle-3-repo]: https://github.com/guzzle/guzzle3 |
||||
|
[guzzle-4-repo]: https://github.com/guzzle/guzzle/tree/4.x |
||||
|
[guzzle-5-repo]: https://github.com/guzzle/guzzle/tree/5.3 |
||||
|
[guzzle-6-repo]: https://github.com/guzzle/guzzle |
||||
|
[guzzle-3-docs]: http://guzzle3.readthedocs.org/en/latest/ |
||||
|
[guzzle-5-docs]: http://guzzle.readthedocs.org/en/5.3/ |
||||
|
[guzzle-6-docs]: http://guzzle.readthedocs.org/en/latest/ |
||||
File diff suppressed because it is too large
@ -0,0 +1,44 @@ |
|||||
|
{ |
||||
|
"name": "guzzlehttp/guzzle", |
||||
|
"type": "library", |
||||
|
"description": "Guzzle is a PHP HTTP client library", |
||||
|
"keywords": ["framework", "http", "rest", "web service", "curl", "client", "HTTP client"], |
||||
|
"homepage": "http://guzzlephp.org/", |
||||
|
"license": "MIT", |
||||
|
"authors": [ |
||||
|
{ |
||||
|
"name": "Michael Dowling", |
||||
|
"email": "mtdowling@gmail.com", |
||||
|
"homepage": "https://github.com/mtdowling" |
||||
|
} |
||||
|
], |
||||
|
"require": { |
||||
|
"php": ">=5.5", |
||||
|
"guzzlehttp/psr7": "^1.4", |
||||
|
"guzzlehttp/promises": "^1.0" |
||||
|
}, |
||||
|
"require-dev": { |
||||
|
"ext-curl": "*", |
||||
|
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", |
||||
|
"psr/log": "^1.0" |
||||
|
}, |
||||
|
"autoload": { |
||||
|
"files": ["src/functions_include.php"], |
||||
|
"psr-4": { |
||||
|
"GuzzleHttp\\": "src/" |
||||
|
} |
||||
|
}, |
||||
|
"autoload-dev": { |
||||
|
"psr-4": { |
||||
|
"GuzzleHttp\\Tests\\": "tests/" |
||||
|
} |
||||
|
}, |
||||
|
"suggest": { |
||||
|
"psr/log": "Required for using the Log middleware" |
||||
|
}, |
||||
|
"extra": { |
||||
|
"branch-alias": { |
||||
|
"dev-master": "6.3-dev" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,422 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp; |
||||
|
|
||||
|
use GuzzleHttp\Cookie\CookieJar; |
||||
|
use GuzzleHttp\Promise; |
||||
|
use GuzzleHttp\Psr7; |
||||
|
use Psr\Http\Message\UriInterface; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
|
||||
|
/** |
||||
|
* @method ResponseInterface get(string|UriInterface $uri, array $options = []) |
||||
|
* @method ResponseInterface head(string|UriInterface $uri, array $options = []) |
||||
|
* @method ResponseInterface put(string|UriInterface $uri, array $options = []) |
||||
|
* @method ResponseInterface post(string|UriInterface $uri, array $options = []) |
||||
|
* @method ResponseInterface patch(string|UriInterface $uri, array $options = []) |
||||
|
* @method ResponseInterface delete(string|UriInterface $uri, array $options = []) |
||||
|
* @method Promise\PromiseInterface getAsync(string|UriInterface $uri, array $options = []) |
||||
|
* @method Promise\PromiseInterface headAsync(string|UriInterface $uri, array $options = []) |
||||
|
* @method Promise\PromiseInterface putAsync(string|UriInterface $uri, array $options = []) |
||||
|
* @method Promise\PromiseInterface postAsync(string|UriInterface $uri, array $options = []) |
||||
|
* @method Promise\PromiseInterface patchAsync(string|UriInterface $uri, array $options = []) |
||||
|
* @method Promise\PromiseInterface deleteAsync(string|UriInterface $uri, array $options = []) |
||||
|
*/ |
||||
|
class Client implements ClientInterface |
||||
|
{ |
||||
|
/** @var array Default request options */ |
||||
|
private $config; |
||||
|
|
||||
|
/** |
||||
|
* Clients accept an array of constructor parameters. |
||||
|
* |
||||
|
* Here's an example of creating a client using a base_uri and an array of |
||||
|
* default request options to apply to each request: |
||||
|
* |
||||
|
* $client = new Client([ |
||||
|
* 'base_uri' => 'http://www.foo.com/1.0/', |
||||
|
* 'timeout' => 0, |
||||
|
* 'allow_redirects' => false, |
||||
|
* 'proxy' => '192.168.16.1:10' |
||||
|
* ]); |
||||
|
* |
||||
|
* Client configuration settings include the following options: |
||||
|
* |
||||
|
* - handler: (callable) Function that transfers HTTP requests over the |
||||
|
* wire. The function is called with a Psr7\Http\Message\RequestInterface |
||||
|
* and array of transfer options, and must return a |
||||
|
* GuzzleHttp\Promise\PromiseInterface that is fulfilled with a |
||||
|
* Psr7\Http\Message\ResponseInterface on success. "handler" is a |
||||
|
* constructor only option that cannot be overridden in per/request |
||||
|
* options. If no handler is provided, a default handler will be created |
||||
|
* that enables all of the request options below by attaching all of the |
||||
|
* default middleware to the handler. |
||||
|
* - base_uri: (string|UriInterface) Base URI of the client that is merged |
||||
|
* into relative URIs. Can be a string or instance of UriInterface. |
||||
|
* - **: any request option |
||||
|
* |
||||
|
* @param array $config Client configuration settings. |
||||
|
* |
||||
|
* @see \GuzzleHttp\RequestOptions for a list of available request options. |
||||
|
*/ |
||||
|
public function __construct(array $config = []) |
||||
|
{ |
||||
|
if (!isset($config['handler'])) { |
||||
|
$config['handler'] = HandlerStack::create(); |
||||
|
} elseif (!is_callable($config['handler'])) { |
||||
|
throw new \InvalidArgumentException('handler must be a callable'); |
||||
|
} |
||||
|
|
||||
|
// Convert the base_uri to a UriInterface |
||||
|
if (isset($config['base_uri'])) { |
||||
|
$config['base_uri'] = Psr7\uri_for($config['base_uri']); |
||||
|
} |
||||
|
|
||||
|
$this->configureDefaults($config); |
||||
|
} |
||||
|
|
||||
|
public function __call($method, $args) |
||||
|
{ |
||||
|
if (count($args) < 1) { |
||||
|
throw new \InvalidArgumentException('Magic request methods require a URI and optional options array'); |
||||
|
} |
||||
|
|
||||
|
$uri = $args[0]; |
||||
|
$opts = isset($args[1]) ? $args[1] : []; |
||||
|
|
||||
|
return substr($method, -5) === 'Async' |
||||
|
? $this->requestAsync(substr($method, 0, -5), $uri, $opts) |
||||
|
: $this->request($method, $uri, $opts); |
||||
|
} |
||||
|
|
||||
|
public function sendAsync(RequestInterface $request, array $options = []) |
||||
|
{ |
||||
|
// Merge the base URI into the request URI if needed. |
||||
|
$options = $this->prepareDefaults($options); |
||||
|
|
||||
|
return $this->transfer( |
||||
|
$request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')), |
||||
|
$options |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public function send(RequestInterface $request, array $options = []) |
||||
|
{ |
||||
|
$options[RequestOptions::SYNCHRONOUS] = true; |
||||
|
return $this->sendAsync($request, $options)->wait(); |
||||
|
} |
||||
|
|
||||
|
public function requestAsync($method, $uri = '', array $options = []) |
||||
|
{ |
||||
|
$options = $this->prepareDefaults($options); |
||||
|
// Remove request modifying parameter because it can be done up-front. |
||||
|
$headers = isset($options['headers']) ? $options['headers'] : []; |
||||
|
$body = isset($options['body']) ? $options['body'] : null; |
||||
|
$version = isset($options['version']) ? $options['version'] : '1.1'; |
||||
|
// Merge the URI into the base URI. |
||||
|
$uri = $this->buildUri($uri, $options); |
||||
|
if (is_array($body)) { |
||||
|
$this->invalidBody(); |
||||
|
} |
||||
|
$request = new Psr7\Request($method, $uri, $headers, $body, $version); |
||||
|
// Remove the option so that they are not doubly-applied. |
||||
|
unset($options['headers'], $options['body'], $options['version']); |
||||
|
|
||||
|
return $this->transfer($request, $options); |
||||
|
} |
||||
|
|
||||
|
public function request($method, $uri = '', array $options = []) |
||||
|
{ |
||||
|
$options[RequestOptions::SYNCHRONOUS] = true; |
||||
|
return $this->requestAsync($method, $uri, $options)->wait(); |
||||
|
} |
||||
|
|
||||
|
public function getConfig($option = null) |
||||
|
{ |
||||
|
return $option === null |
||||
|
? $this->config |
||||
|
: (isset($this->config[$option]) ? $this->config[$option] : null); |
||||
|
} |
||||
|
|
||||
|
private function buildUri($uri, array $config) |
||||
|
{ |
||||
|
// for BC we accept null which would otherwise fail in uri_for |
||||
|
$uri = Psr7\uri_for($uri === null ? '' : $uri); |
||||
|
|
||||
|
if (isset($config['base_uri'])) { |
||||
|
$uri = Psr7\UriResolver::resolve(Psr7\uri_for($config['base_uri']), $uri); |
||||
|
} |
||||
|
|
||||
|
return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Configures the default options for a client. |
||||
|
* |
||||
|
* @param array $config |
||||
|
*/ |
||||
|
private function configureDefaults(array $config) |
||||
|
{ |
||||
|
$defaults = [ |
||||
|
'allow_redirects' => RedirectMiddleware::$defaultSettings, |
||||
|
'http_errors' => true, |
||||
|
'decode_content' => true, |
||||
|
'verify' => true, |
||||
|
'cookies' => false |
||||
|
]; |
||||
|
|
||||
|
// Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set. |
||||
|
|
||||
|
// We can only trust the HTTP_PROXY environment variable in a CLI |
||||
|
// process due to the fact that PHP has no reliable mechanism to |
||||
|
// get environment variables that start with "HTTP_". |
||||
|
if (php_sapi_name() == 'cli' && getenv('HTTP_PROXY')) { |
||||
|
$defaults['proxy']['http'] = getenv('HTTP_PROXY'); |
||||
|
} |
||||
|
|
||||
|
if ($proxy = getenv('HTTPS_PROXY')) { |
||||
|
$defaults['proxy']['https'] = $proxy; |
||||
|
} |
||||
|
|
||||
|
if ($noProxy = getenv('NO_PROXY')) { |
||||
|
$cleanedNoProxy = str_replace(' ', '', $noProxy); |
||||
|
$defaults['proxy']['no'] = explode(',', $cleanedNoProxy); |
||||
|
} |
||||
|
|
||||
|
$this->config = $config + $defaults; |
||||
|
|
||||
|
if (!empty($config['cookies']) && $config['cookies'] === true) { |
||||
|
$this->config['cookies'] = new CookieJar(); |
||||
|
} |
||||
|
|
||||
|
// Add the default user-agent header. |
||||
|
if (!isset($this->config['headers'])) { |
||||
|
$this->config['headers'] = ['User-Agent' => default_user_agent()]; |
||||
|
} else { |
||||
|
// Add the User-Agent header if one was not already set. |
||||
|
foreach (array_keys($this->config['headers']) as $name) { |
||||
|
if (strtolower($name) === 'user-agent') { |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
$this->config['headers']['User-Agent'] = default_user_agent(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Merges default options into the array. |
||||
|
* |
||||
|
* @param array $options Options to modify by reference |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
private function prepareDefaults($options) |
||||
|
{ |
||||
|
$defaults = $this->config; |
||||
|
|
||||
|
if (!empty($defaults['headers'])) { |
||||
|
// Default headers are only added if they are not present. |
||||
|
$defaults['_conditional'] = $defaults['headers']; |
||||
|
unset($defaults['headers']); |
||||
|
} |
||||
|
|
||||
|
// Special handling for headers is required as they are added as |
||||
|
// conditional headers and as headers passed to a request ctor. |
||||
|
if (array_key_exists('headers', $options)) { |
||||
|
// Allows default headers to be unset. |
||||
|
if ($options['headers'] === null) { |
||||
|
$defaults['_conditional'] = null; |
||||
|
unset($options['headers']); |
||||
|
} elseif (!is_array($options['headers'])) { |
||||
|
throw new \InvalidArgumentException('headers must be an array'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Shallow merge defaults underneath options. |
||||
|
$result = $options + $defaults; |
||||
|
|
||||
|
// Remove null values. |
||||
|
foreach ($result as $k => $v) { |
||||
|
if ($v === null) { |
||||
|
unset($result[$k]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Transfers the given request and applies request options. |
||||
|
* |
||||
|
* The URI of the request is not modified and the request options are used |
||||
|
* as-is without merging in default options. |
||||
|
* |
||||
|
* @param RequestInterface $request |
||||
|
* @param array $options |
||||
|
* |
||||
|
* @return Promise\PromiseInterface |
||||
|
*/ |
||||
|
private function transfer(RequestInterface $request, array $options) |
||||
|
{ |
||||
|
// save_to -> sink |
||||
|
if (isset($options['save_to'])) { |
||||
|
$options['sink'] = $options['save_to']; |
||||
|
unset($options['save_to']); |
||||
|
} |
||||
|
|
||||
|
// exceptions -> http_errors |
||||
|
if (isset($options['exceptions'])) { |
||||
|
$options['http_errors'] = $options['exceptions']; |
||||
|
unset($options['exceptions']); |
||||
|
} |
||||
|
|
||||
|
$request = $this->applyOptions($request, $options); |
||||
|
$handler = $options['handler']; |
||||
|
|
||||
|
try { |
||||
|
return Promise\promise_for($handler($request, $options)); |
||||
|
} catch (\Exception $e) { |
||||
|
return Promise\rejection_for($e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Applies the array of request options to a request. |
||||
|
* |
||||
|
* @param RequestInterface $request |
||||
|
* @param array $options |
||||
|
* |
||||
|
* @return RequestInterface |
||||
|
*/ |
||||
|
private function applyOptions(RequestInterface $request, array &$options) |
||||
|
{ |
||||
|
$modify = [ |
||||
|
'set_headers' => [], |
||||
|
]; |
||||
|
|
||||
|
if (isset($options['headers'])) { |
||||
|
$modify['set_headers'] = $options['headers']; |
||||
|
unset($options['headers']); |
||||
|
} |
||||
|
|
||||
|
if (isset($options['form_params'])) { |
||||
|
if (isset($options['multipart'])) { |
||||
|
throw new \InvalidArgumentException('You cannot use ' |
||||
|
. 'form_params and multipart at the same time. Use the ' |
||||
|
. 'form_params option if you want to send application/' |
||||
|
. 'x-www-form-urlencoded requests, and the multipart ' |
||||
|
. 'option to send multipart/form-data requests.'); |
||||
|
} |
||||
|
$options['body'] = http_build_query($options['form_params'], '', '&'); |
||||
|
unset($options['form_params']); |
||||
|
// Ensure that we don't have the header in different case and set the new value. |
||||
|
$options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); |
||||
|
$options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded'; |
||||
|
} |
||||
|
|
||||
|
if (isset($options['multipart'])) { |
||||
|
$options['body'] = new Psr7\MultipartStream($options['multipart']); |
||||
|
unset($options['multipart']); |
||||
|
} |
||||
|
|
||||
|
if (isset($options['json'])) { |
||||
|
$options['body'] = \GuzzleHttp\json_encode($options['json']); |
||||
|
unset($options['json']); |
||||
|
// Ensure that we don't have the header in different case and set the new value. |
||||
|
$options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); |
||||
|
$options['_conditional']['Content-Type'] = 'application/json'; |
||||
|
} |
||||
|
|
||||
|
if (!empty($options['decode_content']) |
||||
|
&& $options['decode_content'] !== true |
||||
|
) { |
||||
|
// Ensure that we don't have the header in different case and set the new value. |
||||
|
$options['_conditional'] = Psr7\_caseless_remove(['Accept-Encoding'], $options['_conditional']); |
||||
|
$modify['set_headers']['Accept-Encoding'] = $options['decode_content']; |
||||
|
} |
||||
|
|
||||
|
if (isset($options['body'])) { |
||||
|
if (is_array($options['body'])) { |
||||
|
$this->invalidBody(); |
||||
|
} |
||||
|
$modify['body'] = Psr7\stream_for($options['body']); |
||||
|
unset($options['body']); |
||||
|
} |
||||
|
|
||||
|
if (!empty($options['auth']) && is_array($options['auth'])) { |
||||
|
$value = $options['auth']; |
||||
|
$type = isset($value[2]) ? strtolower($value[2]) : 'basic'; |
||||
|
switch ($type) { |
||||
|
case 'basic': |
||||
|
// Ensure that we don't have the header in different case and set the new value. |
||||
|
$modify['set_headers'] = Psr7\_caseless_remove(['Authorization'], $modify['set_headers']); |
||||
|
$modify['set_headers']['Authorization'] = 'Basic ' |
||||
|
. base64_encode("$value[0]:$value[1]"); |
||||
|
break; |
||||
|
case 'digest': |
||||
|
// @todo: Do not rely on curl |
||||
|
$options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST; |
||||
|
$options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]"; |
||||
|
break; |
||||
|
case 'ntlm': |
||||
|
$options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_NTLM; |
||||
|
$options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]"; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (isset($options['query'])) { |
||||
|
$value = $options['query']; |
||||
|
if (is_array($value)) { |
||||
|
$value = http_build_query($value, null, '&', PHP_QUERY_RFC3986); |
||||
|
} |
||||
|
if (!is_string($value)) { |
||||
|
throw new \InvalidArgumentException('query must be a string or array'); |
||||
|
} |
||||
|
$modify['query'] = $value; |
||||
|
unset($options['query']); |
||||
|
} |
||||
|
|
||||
|
// Ensure that sink is not an invalid value. |
||||
|
if (isset($options['sink'])) { |
||||
|
// TODO: Add more sink validation? |
||||
|
if (is_bool($options['sink'])) { |
||||
|
throw new \InvalidArgumentException('sink must not be a boolean'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$request = Psr7\modify_request($request, $modify); |
||||
|
if ($request->getBody() instanceof Psr7\MultipartStream) { |
||||
|
// Use a multipart/form-data POST if a Content-Type is not set. |
||||
|
// Ensure that we don't have the header in different case and set the new value. |
||||
|
$options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); |
||||
|
$options['_conditional']['Content-Type'] = 'multipart/form-data; boundary=' |
||||
|
. $request->getBody()->getBoundary(); |
||||
|
} |
||||
|
|
||||
|
// Merge in conditional headers if they are not present. |
||||
|
if (isset($options['_conditional'])) { |
||||
|
// Build up the changes so it's in a single clone of the message. |
||||
|
$modify = []; |
||||
|
foreach ($options['_conditional'] as $k => $v) { |
||||
|
if (!$request->hasHeader($k)) { |
||||
|
$modify['set_headers'][$k] = $v; |
||||
|
} |
||||
|
} |
||||
|
$request = Psr7\modify_request($request, $modify); |
||||
|
// Don't pass this internal value along to middleware/handlers. |
||||
|
unset($options['_conditional']); |
||||
|
} |
||||
|
|
||||
|
return $request; |
||||
|
} |
||||
|
|
||||
|
private function invalidBody() |
||||
|
{ |
||||
|
throw new \InvalidArgumentException('Passing in the "body" request ' |
||||
|
. 'option as an array to send a POST request has been deprecated. ' |
||||
|
. 'Please use the "form_params" request option to send a ' |
||||
|
. 'application/x-www-form-urlencoded request, or the "multipart" ' |
||||
|
. 'request option to send a multipart/form-data request.'); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp; |
||||
|
|
||||
|
use GuzzleHttp\Promise\PromiseInterface; |
||||
|
use GuzzleHttp\Exception\GuzzleException; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
use Psr\Http\Message\UriInterface; |
||||
|
|
||||
|
/** |
||||
|
* Client interface for sending HTTP requests. |
||||
|
*/ |
||||
|
interface ClientInterface |
||||
|
{ |
||||
|
const VERSION = '6.3.3'; |
||||
|
|
||||
|
/** |
||||
|
* Send an HTTP request. |
||||
|
* |
||||
|
* @param RequestInterface $request Request to send |
||||
|
* @param array $options Request options to apply to the given |
||||
|
* request and to the transfer. |
||||
|
* |
||||
|
* @return ResponseInterface |
||||
|
* @throws GuzzleException |
||||
|
*/ |
||||
|
public function send(RequestInterface $request, array $options = []); |
||||
|
|
||||
|
/** |
||||
|
* Asynchronously send an HTTP request. |
||||
|
* |
||||
|
* @param RequestInterface $request Request to send |
||||
|
* @param array $options Request options to apply to the given |
||||
|
* request and to the transfer. |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
public function sendAsync(RequestInterface $request, array $options = []); |
||||
|
|
||||
|
/** |
||||
|
* Create and send an HTTP request. |
||||
|
* |
||||
|
* Use an absolute path to override the base path of the client, or a |
||||
|
* relative path to append to the base path of the client. The URL can |
||||
|
* contain the query string as well. |
||||
|
* |
||||
|
* @param string $method HTTP method. |
||||
|
* @param string|UriInterface $uri URI object or string. |
||||
|
* @param array $options Request options to apply. |
||||
|
* |
||||
|
* @return ResponseInterface |
||||
|
* @throws GuzzleException |
||||
|
*/ |
||||
|
public function request($method, $uri, array $options = []); |
||||
|
|
||||
|
/** |
||||
|
* Create and send an asynchronous HTTP request. |
||||
|
* |
||||
|
* Use an absolute path to override the base path of the client, or a |
||||
|
* relative path to append to the base path of the client. The URL can |
||||
|
* contain the query string as well. Use an array to provide a URL |
||||
|
* template and additional variables to use in the URL template expansion. |
||||
|
* |
||||
|
* @param string $method HTTP method |
||||
|
* @param string|UriInterface $uri URI object or string. |
||||
|
* @param array $options Request options to apply. |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
public function requestAsync($method, $uri, array $options = []); |
||||
|
|
||||
|
/** |
||||
|
* Get a client configuration option. |
||||
|
* |
||||
|
* These options include default request options of the client, a "handler" |
||||
|
* (if utilized by the concrete client), and a "base_uri" if utilized by |
||||
|
* the concrete client. |
||||
|
* |
||||
|
* @param string|null $option The config option to retrieve. |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function getConfig($option = null); |
||||
|
} |
||||
@ -0,0 +1,314 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Cookie; |
||||
|
|
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
|
||||
|
/** |
||||
|
* Cookie jar that stores cookies as an array |
||||
|
*/ |
||||
|
class CookieJar implements CookieJarInterface |
||||
|
{ |
||||
|
/** @var SetCookie[] Loaded cookie data */ |
||||
|
private $cookies = []; |
||||
|
|
||||
|
/** @var bool */ |
||||
|
private $strictMode; |
||||
|
|
||||
|
/** |
||||
|
* @param bool $strictMode Set to true to throw exceptions when invalid |
||||
|
* cookies are added to the cookie jar. |
||||
|
* @param array $cookieArray Array of SetCookie objects or a hash of |
||||
|
* arrays that can be used with the SetCookie |
||||
|
* constructor |
||||
|
*/ |
||||
|
public function __construct($strictMode = false, $cookieArray = []) |
||||
|
{ |
||||
|
$this->strictMode = $strictMode; |
||||
|
|
||||
|
foreach ($cookieArray as $cookie) { |
||||
|
if (!($cookie instanceof SetCookie)) { |
||||
|
$cookie = new SetCookie($cookie); |
||||
|
} |
||||
|
$this->setCookie($cookie); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Create a new Cookie jar from an associative array and domain. |
||||
|
* |
||||
|
* @param array $cookies Cookies to create the jar from |
||||
|
* @param string $domain Domain to set the cookies to |
||||
|
* |
||||
|
* @return self |
||||
|
*/ |
||||
|
public static function fromArray(array $cookies, $domain) |
||||
|
{ |
||||
|
$cookieJar = new self(); |
||||
|
foreach ($cookies as $name => $value) { |
||||
|
$cookieJar->setCookie(new SetCookie([ |
||||
|
'Domain' => $domain, |
||||
|
'Name' => $name, |
||||
|
'Value' => $value, |
||||
|
'Discard' => true |
||||
|
])); |
||||
|
} |
||||
|
|
||||
|
return $cookieJar; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @deprecated |
||||
|
*/ |
||||
|
public static function getCookieValue($value) |
||||
|
{ |
||||
|
return $value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Evaluate if this cookie should be persisted to storage |
||||
|
* that survives between requests. |
||||
|
* |
||||
|
* @param SetCookie $cookie Being evaluated. |
||||
|
* @param bool $allowSessionCookies If we should persist session cookies |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public static function shouldPersist( |
||||
|
SetCookie $cookie, |
||||
|
$allowSessionCookies = false |
||||
|
) { |
||||
|
if ($cookie->getExpires() || $allowSessionCookies) { |
||||
|
if (!$cookie->getDiscard()) { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Finds and returns the cookie based on the name |
||||
|
* |
||||
|
* @param string $name cookie name to search for |
||||
|
* @return SetCookie|null cookie that was found or null if not found |
||||
|
*/ |
||||
|
public function getCookieByName($name) |
||||
|
{ |
||||
|
// don't allow a null name |
||||
|
if ($name === null) { |
||||
|
return null; |
||||
|
} |
||||
|
foreach ($this->cookies as $cookie) { |
||||
|
if ($cookie->getName() !== null && strcasecmp($cookie->getName(), $name) === 0) { |
||||
|
return $cookie; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function toArray() |
||||
|
{ |
||||
|
return array_map(function (SetCookie $cookie) { |
||||
|
return $cookie->toArray(); |
||||
|
}, $this->getIterator()->getArrayCopy()); |
||||
|
} |
||||
|
|
||||
|
public function clear($domain = null, $path = null, $name = null) |
||||
|
{ |
||||
|
if (!$domain) { |
||||
|
$this->cookies = []; |
||||
|
return; |
||||
|
} elseif (!$path) { |
||||
|
$this->cookies = array_filter( |
||||
|
$this->cookies, |
||||
|
function (SetCookie $cookie) use ($path, $domain) { |
||||
|
return !$cookie->matchesDomain($domain); |
||||
|
} |
||||
|
); |
||||
|
} elseif (!$name) { |
||||
|
$this->cookies = array_filter( |
||||
|
$this->cookies, |
||||
|
function (SetCookie $cookie) use ($path, $domain) { |
||||
|
return !($cookie->matchesPath($path) && |
||||
|
$cookie->matchesDomain($domain)); |
||||
|
} |
||||
|
); |
||||
|
} else { |
||||
|
$this->cookies = array_filter( |
||||
|
$this->cookies, |
||||
|
function (SetCookie $cookie) use ($path, $domain, $name) { |
||||
|
return !($cookie->getName() == $name && |
||||
|
$cookie->matchesPath($path) && |
||||
|
$cookie->matchesDomain($domain)); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function clearSessionCookies() |
||||
|
{ |
||||
|
$this->cookies = array_filter( |
||||
|
$this->cookies, |
||||
|
function (SetCookie $cookie) { |
||||
|
return !$cookie->getDiscard() && $cookie->getExpires(); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public function setCookie(SetCookie $cookie) |
||||
|
{ |
||||
|
// If the name string is empty (but not 0), ignore the set-cookie |
||||
|
// string entirely. |
||||
|
$name = $cookie->getName(); |
||||
|
if (!$name && $name !== '0') { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Only allow cookies with set and valid domain, name, value |
||||
|
$result = $cookie->validate(); |
||||
|
if ($result !== true) { |
||||
|
if ($this->strictMode) { |
||||
|
throw new \RuntimeException('Invalid cookie: ' . $result); |
||||
|
} else { |
||||
|
$this->removeCookieIfEmpty($cookie); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Resolve conflicts with previously set cookies |
||||
|
foreach ($this->cookies as $i => $c) { |
||||
|
|
||||
|
// Two cookies are identical, when their path, and domain are |
||||
|
// identical. |
||||
|
if ($c->getPath() != $cookie->getPath() || |
||||
|
$c->getDomain() != $cookie->getDomain() || |
||||
|
$c->getName() != $cookie->getName() |
||||
|
) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// The previously set cookie is a discard cookie and this one is |
||||
|
// not so allow the new cookie to be set |
||||
|
if (!$cookie->getDiscard() && $c->getDiscard()) { |
||||
|
unset($this->cookies[$i]); |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// If the new cookie's expiration is further into the future, then |
||||
|
// replace the old cookie |
||||
|
if ($cookie->getExpires() > $c->getExpires()) { |
||||
|
unset($this->cookies[$i]); |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// If the value has changed, we better change it |
||||
|
if ($cookie->getValue() !== $c->getValue()) { |
||||
|
unset($this->cookies[$i]); |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// The cookie exists, so no need to continue |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
$this->cookies[] = $cookie; |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public function count() |
||||
|
{ |
||||
|
return count($this->cookies); |
||||
|
} |
||||
|
|
||||
|
public function getIterator() |
||||
|
{ |
||||
|
return new \ArrayIterator(array_values($this->cookies)); |
||||
|
} |
||||
|
|
||||
|
public function extractCookies( |
||||
|
RequestInterface $request, |
||||
|
ResponseInterface $response |
||||
|
) { |
||||
|
if ($cookieHeader = $response->getHeader('Set-Cookie')) { |
||||
|
foreach ($cookieHeader as $cookie) { |
||||
|
$sc = SetCookie::fromString($cookie); |
||||
|
if (!$sc->getDomain()) { |
||||
|
$sc->setDomain($request->getUri()->getHost()); |
||||
|
} |
||||
|
if (0 !== strpos($sc->getPath(), '/')) { |
||||
|
$sc->setPath($this->getCookiePathFromRequest($request)); |
||||
|
} |
||||
|
$this->setCookie($sc); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Computes cookie path following RFC 6265 section 5.1.4 |
||||
|
* |
||||
|
* @link https://tools.ietf.org/html/rfc6265#section-5.1.4 |
||||
|
* |
||||
|
* @param RequestInterface $request |
||||
|
* @return string |
||||
|
*/ |
||||
|
private function getCookiePathFromRequest(RequestInterface $request) |
||||
|
{ |
||||
|
$uriPath = $request->getUri()->getPath(); |
||||
|
if ('' === $uriPath) { |
||||
|
return '/'; |
||||
|
} |
||||
|
if (0 !== strpos($uriPath, '/')) { |
||||
|
return '/'; |
||||
|
} |
||||
|
if ('/' === $uriPath) { |
||||
|
return '/'; |
||||
|
} |
||||
|
if (0 === $lastSlashPos = strrpos($uriPath, '/')) { |
||||
|
return '/'; |
||||
|
} |
||||
|
|
||||
|
return substr($uriPath, 0, $lastSlashPos); |
||||
|
} |
||||
|
|
||||
|
public function withCookieHeader(RequestInterface $request) |
||||
|
{ |
||||
|
$values = []; |
||||
|
$uri = $request->getUri(); |
||||
|
$scheme = $uri->getScheme(); |
||||
|
$host = $uri->getHost(); |
||||
|
$path = $uri->getPath() ?: '/'; |
||||
|
|
||||
|
foreach ($this->cookies as $cookie) { |
||||
|
if ($cookie->matchesPath($path) && |
||||
|
$cookie->matchesDomain($host) && |
||||
|
!$cookie->isExpired() && |
||||
|
(!$cookie->getSecure() || $scheme === 'https') |
||||
|
) { |
||||
|
$values[] = $cookie->getName() . '=' |
||||
|
. $cookie->getValue(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $values |
||||
|
? $request->withHeader('Cookie', implode('; ', $values)) |
||||
|
: $request; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* If a cookie already exists and the server asks to set it again with a |
||||
|
* null value, the cookie must be deleted. |
||||
|
* |
||||
|
* @param SetCookie $cookie |
||||
|
*/ |
||||
|
private function removeCookieIfEmpty(SetCookie $cookie) |
||||
|
{ |
||||
|
$cookieValue = $cookie->getValue(); |
||||
|
if ($cookieValue === null || $cookieValue === '') { |
||||
|
$this->clear( |
||||
|
$cookie->getDomain(), |
||||
|
$cookie->getPath(), |
||||
|
$cookie->getName() |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Cookie; |
||||
|
|
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
|
||||
|
/** |
||||
|
* Stores HTTP cookies. |
||||
|
* |
||||
|
* It extracts cookies from HTTP requests, and returns them in HTTP responses. |
||||
|
* CookieJarInterface instances automatically expire contained cookies when |
||||
|
* necessary. Subclasses are also responsible for storing and retrieving |
||||
|
* cookies from a file, database, etc. |
||||
|
* |
||||
|
* @link http://docs.python.org/2/library/cookielib.html Inspiration |
||||
|
*/ |
||||
|
interface CookieJarInterface extends \Countable, \IteratorAggregate |
||||
|
{ |
||||
|
/** |
||||
|
* Create a request with added cookie headers. |
||||
|
* |
||||
|
* If no matching cookies are found in the cookie jar, then no Cookie |
||||
|
* header is added to the request and the same request is returned. |
||||
|
* |
||||
|
* @param RequestInterface $request Request object to modify. |
||||
|
* |
||||
|
* @return RequestInterface returns the modified request. |
||||
|
*/ |
||||
|
public function withCookieHeader(RequestInterface $request); |
||||
|
|
||||
|
/** |
||||
|
* Extract cookies from an HTTP response and store them in the CookieJar. |
||||
|
* |
||||
|
* @param RequestInterface $request Request that was sent |
||||
|
* @param ResponseInterface $response Response that was received |
||||
|
*/ |
||||
|
public function extractCookies( |
||||
|
RequestInterface $request, |
||||
|
ResponseInterface $response |
||||
|
); |
||||
|
|
||||
|
/** |
||||
|
* Sets a cookie in the cookie jar. |
||||
|
* |
||||
|
* @param SetCookie $cookie Cookie to set. |
||||
|
* |
||||
|
* @return bool Returns true on success or false on failure |
||||
|
*/ |
||||
|
public function setCookie(SetCookie $cookie); |
||||
|
|
||||
|
/** |
||||
|
* Remove cookies currently held in the cookie jar. |
||||
|
* |
||||
|
* Invoking this method without arguments will empty the whole cookie jar. |
||||
|
* If given a $domain argument only cookies belonging to that domain will |
||||
|
* be removed. If given a $domain and $path argument, cookies belonging to |
||||
|
* the specified path within that domain are removed. If given all three |
||||
|
* arguments, then the cookie with the specified name, path and domain is |
||||
|
* removed. |
||||
|
* |
||||
|
* @param string $domain Clears cookies matching a domain |
||||
|
* @param string $path Clears cookies matching a domain and path |
||||
|
* @param string $name Clears cookies matching a domain, path, and name |
||||
|
* |
||||
|
* @return CookieJarInterface |
||||
|
*/ |
||||
|
public function clear($domain = null, $path = null, $name = null); |
||||
|
|
||||
|
/** |
||||
|
* Discard all sessions cookies. |
||||
|
* |
||||
|
* Removes cookies that don't have an expire field or a have a discard |
||||
|
* field set to true. To be called when the user agent shuts down according |
||||
|
* to RFC 2965. |
||||
|
*/ |
||||
|
public function clearSessionCookies(); |
||||
|
|
||||
|
/** |
||||
|
* Converts the cookie jar to an array. |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function toArray(); |
||||
|
} |
||||
@ -0,0 +1,90 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Cookie; |
||||
|
|
||||
|
/** |
||||
|
* Persists non-session cookies using a JSON formatted file |
||||
|
*/ |
||||
|
class FileCookieJar extends CookieJar |
||||
|
{ |
||||
|
/** @var string filename */ |
||||
|
private $filename; |
||||
|
|
||||
|
/** @var bool Control whether to persist session cookies or not. */ |
||||
|
private $storeSessionCookies; |
||||
|
|
||||
|
/** |
||||
|
* Create a new FileCookieJar object |
||||
|
* |
||||
|
* @param string $cookieFile File to store the cookie data |
||||
|
* @param bool $storeSessionCookies Set to true to store session cookies |
||||
|
* in the cookie jar. |
||||
|
* |
||||
|
* @throws \RuntimeException if the file cannot be found or created |
||||
|
*/ |
||||
|
public function __construct($cookieFile, $storeSessionCookies = false) |
||||
|
{ |
||||
|
$this->filename = $cookieFile; |
||||
|
$this->storeSessionCookies = $storeSessionCookies; |
||||
|
|
||||
|
if (file_exists($cookieFile)) { |
||||
|
$this->load($cookieFile); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Saves the file when shutting down |
||||
|
*/ |
||||
|
public function __destruct() |
||||
|
{ |
||||
|
$this->save($this->filename); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Saves the cookies to a file. |
||||
|
* |
||||
|
* @param string $filename File to save |
||||
|
* @throws \RuntimeException if the file cannot be found or created |
||||
|
*/ |
||||
|
public function save($filename) |
||||
|
{ |
||||
|
$json = []; |
||||
|
foreach ($this as $cookie) { |
||||
|
/** @var SetCookie $cookie */ |
||||
|
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { |
||||
|
$json[] = $cookie->toArray(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$jsonStr = \GuzzleHttp\json_encode($json); |
||||
|
if (false === file_put_contents($filename, $jsonStr)) { |
||||
|
throw new \RuntimeException("Unable to save file {$filename}"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Load cookies from a JSON formatted file. |
||||
|
* |
||||
|
* Old cookies are kept unless overwritten by newly loaded ones. |
||||
|
* |
||||
|
* @param string $filename Cookie file to load. |
||||
|
* @throws \RuntimeException if the file cannot be loaded. |
||||
|
*/ |
||||
|
public function load($filename) |
||||
|
{ |
||||
|
$json = file_get_contents($filename); |
||||
|
if (false === $json) { |
||||
|
throw new \RuntimeException("Unable to load file {$filename}"); |
||||
|
} elseif ($json === '') { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
$data = \GuzzleHttp\json_decode($json, true); |
||||
|
if (is_array($data)) { |
||||
|
foreach (json_decode($json, true) as $cookie) { |
||||
|
$this->setCookie(new SetCookie($cookie)); |
||||
|
} |
||||
|
} elseif (strlen($data)) { |
||||
|
throw new \RuntimeException("Invalid cookie file: {$filename}"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Cookie; |
||||
|
|
||||
|
/** |
||||
|
* Persists cookies in the client session |
||||
|
*/ |
||||
|
class SessionCookieJar extends CookieJar |
||||
|
{ |
||||
|
/** @var string session key */ |
||||
|
private $sessionKey; |
||||
|
|
||||
|
/** @var bool Control whether to persist session cookies or not. */ |
||||
|
private $storeSessionCookies; |
||||
|
|
||||
|
/** |
||||
|
* Create a new SessionCookieJar object |
||||
|
* |
||||
|
* @param string $sessionKey Session key name to store the cookie |
||||
|
* data in session |
||||
|
* @param bool $storeSessionCookies Set to true to store session cookies |
||||
|
* in the cookie jar. |
||||
|
*/ |
||||
|
public function __construct($sessionKey, $storeSessionCookies = false) |
||||
|
{ |
||||
|
$this->sessionKey = $sessionKey; |
||||
|
$this->storeSessionCookies = $storeSessionCookies; |
||||
|
$this->load(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Saves cookies to session when shutting down |
||||
|
*/ |
||||
|
public function __destruct() |
||||
|
{ |
||||
|
$this->save(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Save cookies to the client session |
||||
|
*/ |
||||
|
public function save() |
||||
|
{ |
||||
|
$json = []; |
||||
|
foreach ($this as $cookie) { |
||||
|
/** @var SetCookie $cookie */ |
||||
|
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { |
||||
|
$json[] = $cookie->toArray(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$_SESSION[$this->sessionKey] = json_encode($json); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Load the contents of the client session into the data array |
||||
|
*/ |
||||
|
protected function load() |
||||
|
{ |
||||
|
if (!isset($_SESSION[$this->sessionKey])) { |
||||
|
return; |
||||
|
} |
||||
|
$data = json_decode($_SESSION[$this->sessionKey], true); |
||||
|
if (is_array($data)) { |
||||
|
foreach ($data as $cookie) { |
||||
|
$this->setCookie(new SetCookie($cookie)); |
||||
|
} |
||||
|
} elseif (strlen($data)) { |
||||
|
throw new \RuntimeException("Invalid cookie data"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,403 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Cookie; |
||||
|
|
||||
|
/** |
||||
|
* Set-Cookie object |
||||
|
*/ |
||||
|
class SetCookie |
||||
|
{ |
||||
|
/** @var array */ |
||||
|
private static $defaults = [ |
||||
|
'Name' => null, |
||||
|
'Value' => null, |
||||
|
'Domain' => null, |
||||
|
'Path' => '/', |
||||
|
'Max-Age' => null, |
||||
|
'Expires' => null, |
||||
|
'Secure' => false, |
||||
|
'Discard' => false, |
||||
|
'HttpOnly' => false |
||||
|
]; |
||||
|
|
||||
|
/** @var array Cookie data */ |
||||
|
private $data; |
||||
|
|
||||
|
/** |
||||
|
* Create a new SetCookie object from a string |
||||
|
* |
||||
|
* @param string $cookie Set-Cookie header string |
||||
|
* |
||||
|
* @return self |
||||
|
*/ |
||||
|
public static function fromString($cookie) |
||||
|
{ |
||||
|
// Create the default return array |
||||
|
$data = self::$defaults; |
||||
|
// Explode the cookie string using a series of semicolons |
||||
|
$pieces = array_filter(array_map('trim', explode(';', $cookie))); |
||||
|
// The name of the cookie (first kvp) must exist and include an equal sign. |
||||
|
if (empty($pieces[0]) || !strpos($pieces[0], '=')) { |
||||
|
return new self($data); |
||||
|
} |
||||
|
|
||||
|
// Add the cookie pieces into the parsed data array |
||||
|
foreach ($pieces as $part) { |
||||
|
$cookieParts = explode('=', $part, 2); |
||||
|
$key = trim($cookieParts[0]); |
||||
|
$value = isset($cookieParts[1]) |
||||
|
? trim($cookieParts[1], " \n\r\t\0\x0B") |
||||
|
: true; |
||||
|
|
||||
|
// Only check for non-cookies when cookies have been found |
||||
|
if (empty($data['Name'])) { |
||||
|
$data['Name'] = $key; |
||||
|
$data['Value'] = $value; |
||||
|
} else { |
||||
|
foreach (array_keys(self::$defaults) as $search) { |
||||
|
if (!strcasecmp($search, $key)) { |
||||
|
$data[$search] = $value; |
||||
|
continue 2; |
||||
|
} |
||||
|
} |
||||
|
$data[$key] = $value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new self($data); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param array $data Array of cookie data provided by a Cookie parser |
||||
|
*/ |
||||
|
public function __construct(array $data = []) |
||||
|
{ |
||||
|
$this->data = array_replace(self::$defaults, $data); |
||||
|
// Extract the Expires value and turn it into a UNIX timestamp if needed |
||||
|
if (!$this->getExpires() && $this->getMaxAge()) { |
||||
|
// Calculate the Expires date |
||||
|
$this->setExpires(time() + $this->getMaxAge()); |
||||
|
} elseif ($this->getExpires() && !is_numeric($this->getExpires())) { |
||||
|
$this->setExpires($this->getExpires()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function __toString() |
||||
|
{ |
||||
|
$str = $this->data['Name'] . '=' . $this->data['Value'] . '; '; |
||||
|
foreach ($this->data as $k => $v) { |
||||
|
if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) { |
||||
|
if ($k === 'Expires') { |
||||
|
$str .= 'Expires=' . gmdate('D, d M Y H:i:s \G\M\T', $v) . '; '; |
||||
|
} else { |
||||
|
$str .= ($v === true ? $k : "{$k}={$v}") . '; '; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return rtrim($str, '; '); |
||||
|
} |
||||
|
|
||||
|
public function toArray() |
||||
|
{ |
||||
|
return $this->data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the cookie name |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getName() |
||||
|
{ |
||||
|
return $this->data['Name']; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the cookie name |
||||
|
* |
||||
|
* @param string $name Cookie name |
||||
|
*/ |
||||
|
public function setName($name) |
||||
|
{ |
||||
|
$this->data['Name'] = $name; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the cookie value |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getValue() |
||||
|
{ |
||||
|
return $this->data['Value']; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the cookie value |
||||
|
* |
||||
|
* @param string $value Cookie value |
||||
|
*/ |
||||
|
public function setValue($value) |
||||
|
{ |
||||
|
$this->data['Value'] = $value; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the domain |
||||
|
* |
||||
|
* @return string|null |
||||
|
*/ |
||||
|
public function getDomain() |
||||
|
{ |
||||
|
return $this->data['Domain']; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the domain of the cookie |
||||
|
* |
||||
|
* @param string $domain |
||||
|
*/ |
||||
|
public function setDomain($domain) |
||||
|
{ |
||||
|
$this->data['Domain'] = $domain; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the path |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getPath() |
||||
|
{ |
||||
|
return $this->data['Path']; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the path of the cookie |
||||
|
* |
||||
|
* @param string $path Path of the cookie |
||||
|
*/ |
||||
|
public function setPath($path) |
||||
|
{ |
||||
|
$this->data['Path'] = $path; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Maximum lifetime of the cookie in seconds |
||||
|
* |
||||
|
* @return int|null |
||||
|
*/ |
||||
|
public function getMaxAge() |
||||
|
{ |
||||
|
return $this->data['Max-Age']; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the max-age of the cookie |
||||
|
* |
||||
|
* @param int $maxAge Max age of the cookie in seconds |
||||
|
*/ |
||||
|
public function setMaxAge($maxAge) |
||||
|
{ |
||||
|
$this->data['Max-Age'] = $maxAge; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* The UNIX timestamp when the cookie Expires |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function getExpires() |
||||
|
{ |
||||
|
return $this->data['Expires']; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the unix timestamp for which the cookie will expire |
||||
|
* |
||||
|
* @param int $timestamp Unix timestamp |
||||
|
*/ |
||||
|
public function setExpires($timestamp) |
||||
|
{ |
||||
|
$this->data['Expires'] = is_numeric($timestamp) |
||||
|
? (int) $timestamp |
||||
|
: strtotime($timestamp); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get whether or not this is a secure cookie |
||||
|
* |
||||
|
* @return null|bool |
||||
|
*/ |
||||
|
public function getSecure() |
||||
|
{ |
||||
|
return $this->data['Secure']; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set whether or not the cookie is secure |
||||
|
* |
||||
|
* @param bool $secure Set to true or false if secure |
||||
|
*/ |
||||
|
public function setSecure($secure) |
||||
|
{ |
||||
|
$this->data['Secure'] = $secure; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get whether or not this is a session cookie |
||||
|
* |
||||
|
* @return null|bool |
||||
|
*/ |
||||
|
public function getDiscard() |
||||
|
{ |
||||
|
return $this->data['Discard']; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set whether or not this is a session cookie |
||||
|
* |
||||
|
* @param bool $discard Set to true or false if this is a session cookie |
||||
|
*/ |
||||
|
public function setDiscard($discard) |
||||
|
{ |
||||
|
$this->data['Discard'] = $discard; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get whether or not this is an HTTP only cookie |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function getHttpOnly() |
||||
|
{ |
||||
|
return $this->data['HttpOnly']; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set whether or not this is an HTTP only cookie |
||||
|
* |
||||
|
* @param bool $httpOnly Set to true or false if this is HTTP only |
||||
|
*/ |
||||
|
public function setHttpOnly($httpOnly) |
||||
|
{ |
||||
|
$this->data['HttpOnly'] = $httpOnly; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if the cookie matches a path value. |
||||
|
* |
||||
|
* A request-path path-matches a given cookie-path if at least one of |
||||
|
* the following conditions holds: |
||||
|
* |
||||
|
* - The cookie-path and the request-path are identical. |
||||
|
* - The cookie-path is a prefix of the request-path, and the last |
||||
|
* character of the cookie-path is %x2F ("/"). |
||||
|
* - The cookie-path is a prefix of the request-path, and the first |
||||
|
* character of the request-path that is not included in the cookie- |
||||
|
* path is a %x2F ("/") character. |
||||
|
* |
||||
|
* @param string $requestPath Path to check against |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function matchesPath($requestPath) |
||||
|
{ |
||||
|
$cookiePath = $this->getPath(); |
||||
|
|
||||
|
// Match on exact matches or when path is the default empty "/" |
||||
|
if ($cookiePath === '/' || $cookiePath == $requestPath) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// Ensure that the cookie-path is a prefix of the request path. |
||||
|
if (0 !== strpos($requestPath, $cookiePath)) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Match if the last character of the cookie-path is "/" |
||||
|
if (substr($cookiePath, -1, 1) === '/') { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// Match if the first character not included in cookie path is "/" |
||||
|
return substr($requestPath, strlen($cookiePath), 1) === '/'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if the cookie matches a domain value |
||||
|
* |
||||
|
* @param string $domain Domain to check against |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function matchesDomain($domain) |
||||
|
{ |
||||
|
// Remove the leading '.' as per spec in RFC 6265. |
||||
|
// http://tools.ietf.org/html/rfc6265#section-5.2.3 |
||||
|
$cookieDomain = ltrim($this->getDomain(), '.'); |
||||
|
|
||||
|
// Domain not set or exact match. |
||||
|
if (!$cookieDomain || !strcasecmp($domain, $cookieDomain)) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// Matching the subdomain according to RFC 6265. |
||||
|
// http://tools.ietf.org/html/rfc6265#section-5.1.3 |
||||
|
if (filter_var($domain, FILTER_VALIDATE_IP)) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return (bool) preg_match('/\.' . preg_quote($cookieDomain, '/') . '$/', $domain); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if the cookie is expired |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function isExpired() |
||||
|
{ |
||||
|
return $this->getExpires() !== null && time() > $this->getExpires(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if the cookie is valid according to RFC 6265 |
||||
|
* |
||||
|
* @return bool|string Returns true if valid or an error message if invalid |
||||
|
*/ |
||||
|
public function validate() |
||||
|
{ |
||||
|
// Names must not be empty, but can be 0 |
||||
|
$name = $this->getName(); |
||||
|
if (empty($name) && !is_numeric($name)) { |
||||
|
return 'The cookie name must not be empty'; |
||||
|
} |
||||
|
|
||||
|
// Check if any of the invalid characters are present in the cookie name |
||||
|
if (preg_match( |
||||
|
'/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/', |
||||
|
$name |
||||
|
)) { |
||||
|
return 'Cookie name must not contain invalid characters: ASCII ' |
||||
|
. 'Control characters (0-31;127), space, tab and the ' |
||||
|
. 'following characters: ()<>@,;:\"/?={}'; |
||||
|
} |
||||
|
|
||||
|
// Value must not be empty, but can be 0 |
||||
|
$value = $this->getValue(); |
||||
|
if (empty($value) && !is_numeric($value)) { |
||||
|
return 'The cookie value must not be empty'; |
||||
|
} |
||||
|
|
||||
|
// Domains must not be empty, but can be 0 |
||||
|
// A "0" is not a valid internet domain, but may be used as server name |
||||
|
// in a private network. |
||||
|
$domain = $this->getDomain(); |
||||
|
if (empty($domain) && !is_numeric($domain)) { |
||||
|
return 'The cookie domain must not be empty'; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Exception; |
||||
|
|
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
|
||||
|
/** |
||||
|
* Exception when an HTTP error occurs (4xx or 5xx error) |
||||
|
*/ |
||||
|
class BadResponseException extends RequestException |
||||
|
{ |
||||
|
public function __construct( |
||||
|
$message, |
||||
|
RequestInterface $request, |
||||
|
ResponseInterface $response = null, |
||||
|
\Exception $previous = null, |
||||
|
array $handlerContext = [] |
||||
|
) { |
||||
|
if (null === $response) { |
||||
|
@trigger_error( |
||||
|
'Instantiating the ' . __CLASS__ . ' class without a Response is deprecated since version 6.3 and will be removed in 7.0.', |
||||
|
E_USER_DEPRECATED |
||||
|
); |
||||
|
} |
||||
|
parent::__construct($message, $request, $response, $previous, $handlerContext); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Exception; |
||||
|
|
||||
|
/** |
||||
|
* Exception when a client error is encountered (4xx codes) |
||||
|
*/ |
||||
|
class ClientException extends BadResponseException {} |
||||
@ -0,0 +1,37 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Exception; |
||||
|
|
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
|
||||
|
/** |
||||
|
* Exception thrown when a connection cannot be established. |
||||
|
* |
||||
|
* Note that no response is present for a ConnectException |
||||
|
*/ |
||||
|
class ConnectException extends RequestException |
||||
|
{ |
||||
|
public function __construct( |
||||
|
$message, |
||||
|
RequestInterface $request, |
||||
|
\Exception $previous = null, |
||||
|
array $handlerContext = [] |
||||
|
) { |
||||
|
parent::__construct($message, $request, null, $previous, $handlerContext); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return null |
||||
|
*/ |
||||
|
public function getResponse() |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function hasResponse() |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Exception; |
||||
|
|
||||
|
/** |
||||
|
* @method string getMessage() |
||||
|
* @method \Throwable|null getPrevious() |
||||
|
* @method mixed getCode() |
||||
|
* @method string getFile() |
||||
|
* @method int getLine() |
||||
|
* @method array getTrace() |
||||
|
* @method string getTraceAsString() |
||||
|
*/ |
||||
|
interface GuzzleException {} |
||||
@ -0,0 +1,217 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Exception; |
||||
|
|
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
use GuzzleHttp\Promise\PromiseInterface; |
||||
|
use Psr\Http\Message\UriInterface; |
||||
|
|
||||
|
/** |
||||
|
* HTTP Request exception |
||||
|
*/ |
||||
|
class RequestException extends TransferException |
||||
|
{ |
||||
|
/** @var RequestInterface */ |
||||
|
private $request; |
||||
|
|
||||
|
/** @var ResponseInterface */ |
||||
|
private $response; |
||||
|
|
||||
|
/** @var array */ |
||||
|
private $handlerContext; |
||||
|
|
||||
|
public function __construct( |
||||
|
$message, |
||||
|
RequestInterface $request, |
||||
|
ResponseInterface $response = null, |
||||
|
\Exception $previous = null, |
||||
|
array $handlerContext = [] |
||||
|
) { |
||||
|
// Set the code of the exception if the response is set and not future. |
||||
|
$code = $response && !($response instanceof PromiseInterface) |
||||
|
? $response->getStatusCode() |
||||
|
: 0; |
||||
|
parent::__construct($message, $code, $previous); |
||||
|
$this->request = $request; |
||||
|
$this->response = $response; |
||||
|
$this->handlerContext = $handlerContext; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Wrap non-RequestExceptions with a RequestException |
||||
|
* |
||||
|
* @param RequestInterface $request |
||||
|
* @param \Exception $e |
||||
|
* |
||||
|
* @return RequestException |
||||
|
*/ |
||||
|
public static function wrapException(RequestInterface $request, \Exception $e) |
||||
|
{ |
||||
|
return $e instanceof RequestException |
||||
|
? $e |
||||
|
: new RequestException($e->getMessage(), $request, null, $e); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Factory method to create a new exception with a normalized error message |
||||
|
* |
||||
|
* @param RequestInterface $request Request |
||||
|
* @param ResponseInterface $response Response received |
||||
|
* @param \Exception $previous Previous exception |
||||
|
* @param array $ctx Optional handler context. |
||||
|
* |
||||
|
* @return self |
||||
|
*/ |
||||
|
public static function create( |
||||
|
RequestInterface $request, |
||||
|
ResponseInterface $response = null, |
||||
|
\Exception $previous = null, |
||||
|
array $ctx = [] |
||||
|
) { |
||||
|
if (!$response) { |
||||
|
return new self( |
||||
|
'Error completing request', |
||||
|
$request, |
||||
|
null, |
||||
|
$previous, |
||||
|
$ctx |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
$level = (int) floor($response->getStatusCode() / 100); |
||||
|
if ($level === 4) { |
||||
|
$label = 'Client error'; |
||||
|
$className = ClientException::class; |
||||
|
} elseif ($level === 5) { |
||||
|
$label = 'Server error'; |
||||
|
$className = ServerException::class; |
||||
|
} else { |
||||
|
$label = 'Unsuccessful request'; |
||||
|
$className = __CLASS__; |
||||
|
} |
||||
|
|
||||
|
$uri = $request->getUri(); |
||||
|
$uri = static::obfuscateUri($uri); |
||||
|
|
||||
|
// Client Error: `GET /` resulted in a `404 Not Found` response: |
||||
|
// <html> ... (truncated) |
||||
|
$message = sprintf( |
||||
|
'%s: `%s %s` resulted in a `%s %s` response', |
||||
|
$label, |
||||
|
$request->getMethod(), |
||||
|
$uri, |
||||
|
$response->getStatusCode(), |
||||
|
$response->getReasonPhrase() |
||||
|
); |
||||
|
|
||||
|
$summary = static::getResponseBodySummary($response); |
||||
|
|
||||
|
if ($summary !== null) { |
||||
|
$message .= ":\n{$summary}\n"; |
||||
|
} |
||||
|
|
||||
|
return new $className($message, $request, $response, $previous, $ctx); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get a short summary of the response |
||||
|
* |
||||
|
* Will return `null` if the response is not printable. |
||||
|
* |
||||
|
* @param ResponseInterface $response |
||||
|
* |
||||
|
* @return string|null |
||||
|
*/ |
||||
|
public static function getResponseBodySummary(ResponseInterface $response) |
||||
|
{ |
||||
|
$body = $response->getBody(); |
||||
|
|
||||
|
if (!$body->isSeekable()) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
$size = $body->getSize(); |
||||
|
|
||||
|
if ($size === 0) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
$summary = $body->read(120); |
||||
|
$body->rewind(); |
||||
|
|
||||
|
if ($size > 120) { |
||||
|
$summary .= ' (truncated...)'; |
||||
|
} |
||||
|
|
||||
|
// Matches any printable character, including unicode characters: |
||||
|
// letters, marks, numbers, punctuation, spacing, and separators. |
||||
|
if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/', $summary)) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return $summary; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Obfuscates URI if there is an username and a password present |
||||
|
* |
||||
|
* @param UriInterface $uri |
||||
|
* |
||||
|
* @return UriInterface |
||||
|
*/ |
||||
|
private static function obfuscateUri($uri) |
||||
|
{ |
||||
|
$userInfo = $uri->getUserInfo(); |
||||
|
|
||||
|
if (false !== ($pos = strpos($userInfo, ':'))) { |
||||
|
return $uri->withUserInfo(substr($userInfo, 0, $pos), '***'); |
||||
|
} |
||||
|
|
||||
|
return $uri; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the request that caused the exception |
||||
|
* |
||||
|
* @return RequestInterface |
||||
|
*/ |
||||
|
public function getRequest() |
||||
|
{ |
||||
|
return $this->request; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the associated response |
||||
|
* |
||||
|
* @return ResponseInterface|null |
||||
|
*/ |
||||
|
public function getResponse() |
||||
|
{ |
||||
|
return $this->response; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if a response was received |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function hasResponse() |
||||
|
{ |
||||
|
return $this->response !== null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get contextual information about the error from the underlying handler. |
||||
|
* |
||||
|
* The contents of this array will vary depending on which handler you are |
||||
|
* using. It may also be just an empty array. Relying on this data will |
||||
|
* couple you to a specific handler, but can give more debug information |
||||
|
* when needed. |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function getHandlerContext() |
||||
|
{ |
||||
|
return $this->handlerContext; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Exception; |
||||
|
|
||||
|
use Psr\Http\Message\StreamInterface; |
||||
|
|
||||
|
/** |
||||
|
* Exception thrown when a seek fails on a stream. |
||||
|
*/ |
||||
|
class SeekException extends \RuntimeException implements GuzzleException |
||||
|
{ |
||||
|
private $stream; |
||||
|
|
||||
|
public function __construct(StreamInterface $stream, $pos = 0, $msg = '') |
||||
|
{ |
||||
|
$this->stream = $stream; |
||||
|
$msg = $msg ?: 'Could not seek the stream to position ' . $pos; |
||||
|
parent::__construct($msg); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return StreamInterface |
||||
|
*/ |
||||
|
public function getStream() |
||||
|
{ |
||||
|
return $this->stream; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Exception; |
||||
|
|
||||
|
/** |
||||
|
* Exception when a server error is encountered (5xx codes) |
||||
|
*/ |
||||
|
class ServerException extends BadResponseException {} |
||||
@ -0,0 +1,4 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Exception; |
||||
|
|
||||
|
class TooManyRedirectsException extends RequestException {} |
||||
@ -0,0 +1,4 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Exception; |
||||
|
|
||||
|
class TransferException extends \RuntimeException implements GuzzleException {} |
||||
@ -0,0 +1,565 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Handler; |
||||
|
|
||||
|
use GuzzleHttp\Exception\RequestException; |
||||
|
use GuzzleHttp\Exception\ConnectException; |
||||
|
use GuzzleHttp\Promise\FulfilledPromise; |
||||
|
use GuzzleHttp\Psr7; |
||||
|
use GuzzleHttp\Psr7\LazyOpenStream; |
||||
|
use GuzzleHttp\TransferStats; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
|
||||
|
/** |
||||
|
* Creates curl resources from a request |
||||
|
*/ |
||||
|
class CurlFactory implements CurlFactoryInterface |
||||
|
{ |
||||
|
/** @var array */ |
||||
|
private $handles = []; |
||||
|
|
||||
|
/** @var int Total number of idle handles to keep in cache */ |
||||
|
private $maxHandles; |
||||
|
|
||||
|
/** |
||||
|
* @param int $maxHandles Maximum number of idle handles. |
||||
|
*/ |
||||
|
public function __construct($maxHandles) |
||||
|
{ |
||||
|
$this->maxHandles = $maxHandles; |
||||
|
} |
||||
|
|
||||
|
public function create(RequestInterface $request, array $options) |
||||
|
{ |
||||
|
if (isset($options['curl']['body_as_string'])) { |
||||
|
$options['_body_as_string'] = $options['curl']['body_as_string']; |
||||
|
unset($options['curl']['body_as_string']); |
||||
|
} |
||||
|
|
||||
|
$easy = new EasyHandle; |
||||
|
$easy->request = $request; |
||||
|
$easy->options = $options; |
||||
|
$conf = $this->getDefaultConf($easy); |
||||
|
$this->applyMethod($easy, $conf); |
||||
|
$this->applyHandlerOptions($easy, $conf); |
||||
|
$this->applyHeaders($easy, $conf); |
||||
|
unset($conf['_headers']); |
||||
|
|
||||
|
// Add handler options from the request configuration options |
||||
|
if (isset($options['curl'])) { |
||||
|
$conf = array_replace($conf, $options['curl']); |
||||
|
} |
||||
|
|
||||
|
$conf[CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy); |
||||
|
$easy->handle = $this->handles |
||||
|
? array_pop($this->handles) |
||||
|
: curl_init(); |
||||
|
curl_setopt_array($easy->handle, $conf); |
||||
|
|
||||
|
return $easy; |
||||
|
} |
||||
|
|
||||
|
public function release(EasyHandle $easy) |
||||
|
{ |
||||
|
$resource = $easy->handle; |
||||
|
unset($easy->handle); |
||||
|
|
||||
|
if (count($this->handles) >= $this->maxHandles) { |
||||
|
curl_close($resource); |
||||
|
} else { |
||||
|
// Remove all callback functions as they can hold onto references |
||||
|
// and are not cleaned up by curl_reset. Using curl_setopt_array |
||||
|
// does not work for some reason, so removing each one |
||||
|
// individually. |
||||
|
curl_setopt($resource, CURLOPT_HEADERFUNCTION, null); |
||||
|
curl_setopt($resource, CURLOPT_READFUNCTION, null); |
||||
|
curl_setopt($resource, CURLOPT_WRITEFUNCTION, null); |
||||
|
curl_setopt($resource, CURLOPT_PROGRESSFUNCTION, null); |
||||
|
curl_reset($resource); |
||||
|
$this->handles[] = $resource; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Completes a cURL transaction, either returning a response promise or a |
||||
|
* rejected promise. |
||||
|
* |
||||
|
* @param callable $handler |
||||
|
* @param EasyHandle $easy |
||||
|
* @param CurlFactoryInterface $factory Dictates how the handle is released |
||||
|
* |
||||
|
* @return \GuzzleHttp\Promise\PromiseInterface |
||||
|
*/ |
||||
|
public static function finish( |
||||
|
callable $handler, |
||||
|
EasyHandle $easy, |
||||
|
CurlFactoryInterface $factory |
||||
|
) { |
||||
|
if (isset($easy->options['on_stats'])) { |
||||
|
self::invokeStats($easy); |
||||
|
} |
||||
|
|
||||
|
if (!$easy->response || $easy->errno) { |
||||
|
return self::finishError($handler, $easy, $factory); |
||||
|
} |
||||
|
|
||||
|
// Return the response if it is present and there is no error. |
||||
|
$factory->release($easy); |
||||
|
|
||||
|
// Rewind the body of the response if possible. |
||||
|
$body = $easy->response->getBody(); |
||||
|
if ($body->isSeekable()) { |
||||
|
$body->rewind(); |
||||
|
} |
||||
|
|
||||
|
return new FulfilledPromise($easy->response); |
||||
|
} |
||||
|
|
||||
|
private static function invokeStats(EasyHandle $easy) |
||||
|
{ |
||||
|
$curlStats = curl_getinfo($easy->handle); |
||||
|
$stats = new TransferStats( |
||||
|
$easy->request, |
||||
|
$easy->response, |
||||
|
$curlStats['total_time'], |
||||
|
$easy->errno, |
||||
|
$curlStats |
||||
|
); |
||||
|
call_user_func($easy->options['on_stats'], $stats); |
||||
|
} |
||||
|
|
||||
|
private static function finishError( |
||||
|
callable $handler, |
||||
|
EasyHandle $easy, |
||||
|
CurlFactoryInterface $factory |
||||
|
) { |
||||
|
// Get error information and release the handle to the factory. |
||||
|
$ctx = [ |
||||
|
'errno' => $easy->errno, |
||||
|
'error' => curl_error($easy->handle), |
||||
|
] + curl_getinfo($easy->handle); |
||||
|
$factory->release($easy); |
||||
|
|
||||
|
// Retry when nothing is present or when curl failed to rewind. |
||||
|
if (empty($easy->options['_err_message']) |
||||
|
&& (!$easy->errno || $easy->errno == 65) |
||||
|
) { |
||||
|
return self::retryFailedRewind($handler, $easy, $ctx); |
||||
|
} |
||||
|
|
||||
|
return self::createRejection($easy, $ctx); |
||||
|
} |
||||
|
|
||||
|
private static function createRejection(EasyHandle $easy, array $ctx) |
||||
|
{ |
||||
|
static $connectionErrors = [ |
||||
|
CURLE_OPERATION_TIMEOUTED => true, |
||||
|
CURLE_COULDNT_RESOLVE_HOST => true, |
||||
|
CURLE_COULDNT_CONNECT => true, |
||||
|
CURLE_SSL_CONNECT_ERROR => true, |
||||
|
CURLE_GOT_NOTHING => true, |
||||
|
]; |
||||
|
|
||||
|
// If an exception was encountered during the onHeaders event, then |
||||
|
// return a rejected promise that wraps that exception. |
||||
|
if ($easy->onHeadersException) { |
||||
|
return \GuzzleHttp\Promise\rejection_for( |
||||
|
new RequestException( |
||||
|
'An error was encountered during the on_headers event', |
||||
|
$easy->request, |
||||
|
$easy->response, |
||||
|
$easy->onHeadersException, |
||||
|
$ctx |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
$message = sprintf( |
||||
|
'cURL error %s: %s (%s)', |
||||
|
$ctx['errno'], |
||||
|
$ctx['error'], |
||||
|
'see http://curl.haxx.se/libcurl/c/libcurl-errors.html' |
||||
|
); |
||||
|
|
||||
|
// Create a connection exception if it was a specific error code. |
||||
|
$error = isset($connectionErrors[$easy->errno]) |
||||
|
? new ConnectException($message, $easy->request, null, $ctx) |
||||
|
: new RequestException($message, $easy->request, $easy->response, null, $ctx); |
||||
|
|
||||
|
return \GuzzleHttp\Promise\rejection_for($error); |
||||
|
} |
||||
|
|
||||
|
private function getDefaultConf(EasyHandle $easy) |
||||
|
{ |
||||
|
$conf = [ |
||||
|
'_headers' => $easy->request->getHeaders(), |
||||
|
CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), |
||||
|
CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''), |
||||
|
CURLOPT_RETURNTRANSFER => false, |
||||
|
CURLOPT_HEADER => false, |
||||
|
CURLOPT_CONNECTTIMEOUT => 150, |
||||
|
]; |
||||
|
|
||||
|
if (defined('CURLOPT_PROTOCOLS')) { |
||||
|
$conf[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; |
||||
|
} |
||||
|
|
||||
|
$version = $easy->request->getProtocolVersion(); |
||||
|
if ($version == 1.1) { |
||||
|
$conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; |
||||
|
} elseif ($version == 2.0) { |
||||
|
$conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; |
||||
|
} else { |
||||
|
$conf[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; |
||||
|
} |
||||
|
|
||||
|
return $conf; |
||||
|
} |
||||
|
|
||||
|
private function applyMethod(EasyHandle $easy, array &$conf) |
||||
|
{ |
||||
|
$body = $easy->request->getBody(); |
||||
|
$size = $body->getSize(); |
||||
|
|
||||
|
if ($size === null || $size > 0) { |
||||
|
$this->applyBody($easy->request, $easy->options, $conf); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
$method = $easy->request->getMethod(); |
||||
|
if ($method === 'PUT' || $method === 'POST') { |
||||
|
// See http://tools.ietf.org/html/rfc7230#section-3.3.2 |
||||
|
if (!$easy->request->hasHeader('Content-Length')) { |
||||
|
$conf[CURLOPT_HTTPHEADER][] = 'Content-Length: 0'; |
||||
|
} |
||||
|
} elseif ($method === 'HEAD') { |
||||
|
$conf[CURLOPT_NOBODY] = true; |
||||
|
unset( |
||||
|
$conf[CURLOPT_WRITEFUNCTION], |
||||
|
$conf[CURLOPT_READFUNCTION], |
||||
|
$conf[CURLOPT_FILE], |
||||
|
$conf[CURLOPT_INFILE] |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function applyBody(RequestInterface $request, array $options, array &$conf) |
||||
|
{ |
||||
|
$size = $request->hasHeader('Content-Length') |
||||
|
? (int) $request->getHeaderLine('Content-Length') |
||||
|
: null; |
||||
|
|
||||
|
// Send the body as a string if the size is less than 1MB OR if the |
||||
|
// [curl][body_as_string] request value is set. |
||||
|
if (($size !== null && $size < 1000000) || |
||||
|
!empty($options['_body_as_string']) |
||||
|
) { |
||||
|
$conf[CURLOPT_POSTFIELDS] = (string) $request->getBody(); |
||||
|
// Don't duplicate the Content-Length header |
||||
|
$this->removeHeader('Content-Length', $conf); |
||||
|
$this->removeHeader('Transfer-Encoding', $conf); |
||||
|
} else { |
||||
|
$conf[CURLOPT_UPLOAD] = true; |
||||
|
if ($size !== null) { |
||||
|
$conf[CURLOPT_INFILESIZE] = $size; |
||||
|
$this->removeHeader('Content-Length', $conf); |
||||
|
} |
||||
|
$body = $request->getBody(); |
||||
|
if ($body->isSeekable()) { |
||||
|
$body->rewind(); |
||||
|
} |
||||
|
$conf[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) { |
||||
|
return $body->read($length); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// If the Expect header is not present, prevent curl from adding it |
||||
|
if (!$request->hasHeader('Expect')) { |
||||
|
$conf[CURLOPT_HTTPHEADER][] = 'Expect:'; |
||||
|
} |
||||
|
|
||||
|
// cURL sometimes adds a content-type by default. Prevent this. |
||||
|
if (!$request->hasHeader('Content-Type')) { |
||||
|
$conf[CURLOPT_HTTPHEADER][] = 'Content-Type:'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function applyHeaders(EasyHandle $easy, array &$conf) |
||||
|
{ |
||||
|
foreach ($conf['_headers'] as $name => $values) { |
||||
|
foreach ($values as $value) { |
||||
|
$value = (string) $value; |
||||
|
if ($value === '') { |
||||
|
// cURL requires a special format for empty headers. |
||||
|
// See https://github.com/guzzle/guzzle/issues/1882 for more details. |
||||
|
$conf[CURLOPT_HTTPHEADER][] = "$name;"; |
||||
|
} else { |
||||
|
$conf[CURLOPT_HTTPHEADER][] = "$name: $value"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Remove the Accept header if one was not set |
||||
|
if (!$easy->request->hasHeader('Accept')) { |
||||
|
$conf[CURLOPT_HTTPHEADER][] = 'Accept:'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Remove a header from the options array. |
||||
|
* |
||||
|
* @param string $name Case-insensitive header to remove |
||||
|
* @param array $options Array of options to modify |
||||
|
*/ |
||||
|
private function removeHeader($name, array &$options) |
||||
|
{ |
||||
|
foreach (array_keys($options['_headers']) as $key) { |
||||
|
if (!strcasecmp($key, $name)) { |
||||
|
unset($options['_headers'][$key]); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function applyHandlerOptions(EasyHandle $easy, array &$conf) |
||||
|
{ |
||||
|
$options = $easy->options; |
||||
|
if (isset($options['verify'])) { |
||||
|
if ($options['verify'] === false) { |
||||
|
unset($conf[CURLOPT_CAINFO]); |
||||
|
$conf[CURLOPT_SSL_VERIFYHOST] = 0; |
||||
|
$conf[CURLOPT_SSL_VERIFYPEER] = false; |
||||
|
} else { |
||||
|
$conf[CURLOPT_SSL_VERIFYHOST] = 2; |
||||
|
$conf[CURLOPT_SSL_VERIFYPEER] = true; |
||||
|
if (is_string($options['verify'])) { |
||||
|
// Throw an error if the file/folder/link path is not valid or doesn't exist. |
||||
|
if (!file_exists($options['verify'])) { |
||||
|
throw new \InvalidArgumentException( |
||||
|
"SSL CA bundle not found: {$options['verify']}" |
||||
|
); |
||||
|
} |
||||
|
// If it's a directory or a link to a directory use CURLOPT_CAPATH. |
||||
|
// If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO. |
||||
|
if (is_dir($options['verify']) || |
||||
|
(is_link($options['verify']) && is_dir(readlink($options['verify'])))) { |
||||
|
$conf[CURLOPT_CAPATH] = $options['verify']; |
||||
|
} else { |
||||
|
$conf[CURLOPT_CAINFO] = $options['verify']; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!empty($options['decode_content'])) { |
||||
|
$accept = $easy->request->getHeaderLine('Accept-Encoding'); |
||||
|
if ($accept) { |
||||
|
$conf[CURLOPT_ENCODING] = $accept; |
||||
|
} else { |
||||
|
$conf[CURLOPT_ENCODING] = ''; |
||||
|
// Don't let curl send the header over the wire |
||||
|
$conf[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (isset($options['sink'])) { |
||||
|
$sink = $options['sink']; |
||||
|
if (!is_string($sink)) { |
||||
|
$sink = \GuzzleHttp\Psr7\stream_for($sink); |
||||
|
} elseif (!is_dir(dirname($sink))) { |
||||
|
// Ensure that the directory exists before failing in curl. |
||||
|
throw new \RuntimeException(sprintf( |
||||
|
'Directory %s does not exist for sink value of %s', |
||||
|
dirname($sink), |
||||
|
$sink |
||||
|
)); |
||||
|
} else { |
||||
|
$sink = new LazyOpenStream($sink, 'w+'); |
||||
|
} |
||||
|
$easy->sink = $sink; |
||||
|
$conf[CURLOPT_WRITEFUNCTION] = function ($ch, $write) use ($sink) { |
||||
|
return $sink->write($write); |
||||
|
}; |
||||
|
} else { |
||||
|
// Use a default temp stream if no sink was set. |
||||
|
$conf[CURLOPT_FILE] = fopen('php://temp', 'w+'); |
||||
|
$easy->sink = Psr7\stream_for($conf[CURLOPT_FILE]); |
||||
|
} |
||||
|
$timeoutRequiresNoSignal = false; |
||||
|
if (isset($options['timeout'])) { |
||||
|
$timeoutRequiresNoSignal |= $options['timeout'] < 1; |
||||
|
$conf[CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000; |
||||
|
} |
||||
|
|
||||
|
// CURL default value is CURL_IPRESOLVE_WHATEVER |
||||
|
if (isset($options['force_ip_resolve'])) { |
||||
|
if ('v4' === $options['force_ip_resolve']) { |
||||
|
$conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4; |
||||
|
} elseif ('v6' === $options['force_ip_resolve']) { |
||||
|
$conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V6; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (isset($options['connect_timeout'])) { |
||||
|
$timeoutRequiresNoSignal |= $options['connect_timeout'] < 1; |
||||
|
$conf[CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000; |
||||
|
} |
||||
|
|
||||
|
if ($timeoutRequiresNoSignal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { |
||||
|
$conf[CURLOPT_NOSIGNAL] = true; |
||||
|
} |
||||
|
|
||||
|
if (isset($options['proxy'])) { |
||||
|
if (!is_array($options['proxy'])) { |
||||
|
$conf[CURLOPT_PROXY] = $options['proxy']; |
||||
|
} else { |
||||
|
$scheme = $easy->request->getUri()->getScheme(); |
||||
|
if (isset($options['proxy'][$scheme])) { |
||||
|
$host = $easy->request->getUri()->getHost(); |
||||
|
if (!isset($options['proxy']['no']) || |
||||
|
!\GuzzleHttp\is_host_in_noproxy($host, $options['proxy']['no']) |
||||
|
) { |
||||
|
$conf[CURLOPT_PROXY] = $options['proxy'][$scheme]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (isset($options['cert'])) { |
||||
|
$cert = $options['cert']; |
||||
|
if (is_array($cert)) { |
||||
|
$conf[CURLOPT_SSLCERTPASSWD] = $cert[1]; |
||||
|
$cert = $cert[0]; |
||||
|
} |
||||
|
if (!file_exists($cert)) { |
||||
|
throw new \InvalidArgumentException( |
||||
|
"SSL certificate not found: {$cert}" |
||||
|
); |
||||
|
} |
||||
|
$conf[CURLOPT_SSLCERT] = $cert; |
||||
|
} |
||||
|
|
||||
|
if (isset($options['ssl_key'])) { |
||||
|
$sslKey = $options['ssl_key']; |
||||
|
if (is_array($sslKey)) { |
||||
|
$conf[CURLOPT_SSLKEYPASSWD] = $sslKey[1]; |
||||
|
$sslKey = $sslKey[0]; |
||||
|
} |
||||
|
if (!file_exists($sslKey)) { |
||||
|
throw new \InvalidArgumentException( |
||||
|
"SSL private key not found: {$sslKey}" |
||||
|
); |
||||
|
} |
||||
|
$conf[CURLOPT_SSLKEY] = $sslKey; |
||||
|
} |
||||
|
|
||||
|
if (isset($options['progress'])) { |
||||
|
$progress = $options['progress']; |
||||
|
if (!is_callable($progress)) { |
||||
|
throw new \InvalidArgumentException( |
||||
|
'progress client option must be callable' |
||||
|
); |
||||
|
} |
||||
|
$conf[CURLOPT_NOPROGRESS] = false; |
||||
|
$conf[CURLOPT_PROGRESSFUNCTION] = function () use ($progress) { |
||||
|
$args = func_get_args(); |
||||
|
// PHP 5.5 pushed the handle onto the start of the args |
||||
|
if (is_resource($args[0])) { |
||||
|
array_shift($args); |
||||
|
} |
||||
|
call_user_func_array($progress, $args); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
if (!empty($options['debug'])) { |
||||
|
$conf[CURLOPT_STDERR] = \GuzzleHttp\debug_resource($options['debug']); |
||||
|
$conf[CURLOPT_VERBOSE] = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* This function ensures that a response was set on a transaction. If one |
||||
|
* was not set, then the request is retried if possible. This error |
||||
|
* typically means you are sending a payload, curl encountered a |
||||
|
* "Connection died, retrying a fresh connect" error, tried to rewind the |
||||
|
* stream, and then encountered a "necessary data rewind wasn't possible" |
||||
|
* error, causing the request to be sent through curl_multi_info_read() |
||||
|
* without an error status. |
||||
|
*/ |
||||
|
private static function retryFailedRewind( |
||||
|
callable $handler, |
||||
|
EasyHandle $easy, |
||||
|
array $ctx |
||||
|
) { |
||||
|
try { |
||||
|
// Only rewind if the body has been read from. |
||||
|
$body = $easy->request->getBody(); |
||||
|
if ($body->tell() > 0) { |
||||
|
$body->rewind(); |
||||
|
} |
||||
|
} catch (\RuntimeException $e) { |
||||
|
$ctx['error'] = 'The connection unexpectedly failed without ' |
||||
|
. 'providing an error. The request would have been retried, ' |
||||
|
. 'but attempting to rewind the request body failed. ' |
||||
|
. 'Exception: ' . $e; |
||||
|
return self::createRejection($easy, $ctx); |
||||
|
} |
||||
|
|
||||
|
// Retry no more than 3 times before giving up. |
||||
|
if (!isset($easy->options['_curl_retries'])) { |
||||
|
$easy->options['_curl_retries'] = 1; |
||||
|
} elseif ($easy->options['_curl_retries'] == 2) { |
||||
|
$ctx['error'] = 'The cURL request was retried 3 times ' |
||||
|
. 'and did not succeed. The most likely reason for the failure ' |
||||
|
. 'is that cURL was unable to rewind the body of the request ' |
||||
|
. 'and subsequent retries resulted in the same error. Turn on ' |
||||
|
. 'the debug option to see what went wrong. See ' |
||||
|
. 'https://bugs.php.net/bug.php?id=47204 for more information.'; |
||||
|
return self::createRejection($easy, $ctx); |
||||
|
} else { |
||||
|
$easy->options['_curl_retries']++; |
||||
|
} |
||||
|
|
||||
|
return $handler($easy->request, $easy->options); |
||||
|
} |
||||
|
|
||||
|
private function createHeaderFn(EasyHandle $easy) |
||||
|
{ |
||||
|
if (isset($easy->options['on_headers'])) { |
||||
|
$onHeaders = $easy->options['on_headers']; |
||||
|
|
||||
|
if (!is_callable($onHeaders)) { |
||||
|
throw new \InvalidArgumentException('on_headers must be callable'); |
||||
|
} |
||||
|
} else { |
||||
|
$onHeaders = null; |
||||
|
} |
||||
|
|
||||
|
return function ($ch, $h) use ( |
||||
|
$onHeaders, |
||||
|
$easy, |
||||
|
&$startingResponse |
||||
|
) { |
||||
|
$value = trim($h); |
||||
|
if ($value === '') { |
||||
|
$startingResponse = true; |
||||
|
$easy->createResponse(); |
||||
|
if ($onHeaders !== null) { |
||||
|
try { |
||||
|
$onHeaders($easy->response); |
||||
|
} catch (\Exception $e) { |
||||
|
// Associate the exception with the handle and trigger |
||||
|
// a curl header write error by returning 0. |
||||
|
$easy->onHeadersException = $e; |
||||
|
return -1; |
||||
|
} |
||||
|
} |
||||
|
} elseif ($startingResponse) { |
||||
|
$startingResponse = false; |
||||
|
$easy->headers = [$value]; |
||||
|
} else { |
||||
|
$easy->headers[] = $value; |
||||
|
} |
||||
|
return strlen($h); |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Handler; |
||||
|
|
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
|
||||
|
interface CurlFactoryInterface |
||||
|
{ |
||||
|
/** |
||||
|
* Creates a cURL handle resource. |
||||
|
* |
||||
|
* @param RequestInterface $request Request |
||||
|
* @param array $options Transfer options |
||||
|
* |
||||
|
* @return EasyHandle |
||||
|
* @throws \RuntimeException when an option cannot be applied |
||||
|
*/ |
||||
|
public function create(RequestInterface $request, array $options); |
||||
|
|
||||
|
/** |
||||
|
* Release an easy handle, allowing it to be reused or closed. |
||||
|
* |
||||
|
* This function must call unset on the easy handle's "handle" property. |
||||
|
* |
||||
|
* @param EasyHandle $easy |
||||
|
*/ |
||||
|
public function release(EasyHandle $easy); |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Handler; |
||||
|
|
||||
|
use GuzzleHttp\Psr7; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
|
||||
|
/** |
||||
|
* HTTP handler that uses cURL easy handles as a transport layer. |
||||
|
* |
||||
|
* When using the CurlHandler, custom curl options can be specified as an |
||||
|
* associative array of curl option constants mapping to values in the |
||||
|
* **curl** key of the "client" key of the request. |
||||
|
*/ |
||||
|
class CurlHandler |
||||
|
{ |
||||
|
/** @var CurlFactoryInterface */ |
||||
|
private $factory; |
||||
|
|
||||
|
/** |
||||
|
* Accepts an associative array of options: |
||||
|
* |
||||
|
* - factory: Optional curl factory used to create cURL handles. |
||||
|
* |
||||
|
* @param array $options Array of options to use with the handler |
||||
|
*/ |
||||
|
public function __construct(array $options = []) |
||||
|
{ |
||||
|
$this->factory = isset($options['handle_factory']) |
||||
|
? $options['handle_factory'] |
||||
|
: new CurlFactory(3); |
||||
|
} |
||||
|
|
||||
|
public function __invoke(RequestInterface $request, array $options) |
||||
|
{ |
||||
|
if (isset($options['delay'])) { |
||||
|
usleep($options['delay'] * 1000); |
||||
|
} |
||||
|
|
||||
|
$easy = $this->factory->create($request, $options); |
||||
|
curl_exec($easy->handle); |
||||
|
$easy->errno = curl_errno($easy->handle); |
||||
|
|
||||
|
return CurlFactory::finish($this, $easy, $this->factory); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,199 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Handler; |
||||
|
|
||||
|
use GuzzleHttp\Promise as P; |
||||
|
use GuzzleHttp\Promise\Promise; |
||||
|
use GuzzleHttp\Psr7; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
|
||||
|
/** |
||||
|
* Returns an asynchronous response using curl_multi_* functions. |
||||
|
* |
||||
|
* When using the CurlMultiHandler, custom curl options can be specified as an |
||||
|
* associative array of curl option constants mapping to values in the |
||||
|
* **curl** key of the provided request options. |
||||
|
* |
||||
|
* @property resource $_mh Internal use only. Lazy loaded multi-handle. |
||||
|
*/ |
||||
|
class CurlMultiHandler |
||||
|
{ |
||||
|
/** @var CurlFactoryInterface */ |
||||
|
private $factory; |
||||
|
private $selectTimeout; |
||||
|
private $active; |
||||
|
private $handles = []; |
||||
|
private $delays = []; |
||||
|
|
||||
|
/** |
||||
|
* This handler accepts the following options: |
||||
|
* |
||||
|
* - handle_factory: An optional factory used to create curl handles |
||||
|
* - select_timeout: Optional timeout (in seconds) to block before timing |
||||
|
* out while selecting curl handles. Defaults to 1 second. |
||||
|
* |
||||
|
* @param array $options |
||||
|
*/ |
||||
|
public function __construct(array $options = []) |
||||
|
{ |
||||
|
$this->factory = isset($options['handle_factory']) |
||||
|
? $options['handle_factory'] : new CurlFactory(50); |
||||
|
$this->selectTimeout = isset($options['select_timeout']) |
||||
|
? $options['select_timeout'] : 1; |
||||
|
} |
||||
|
|
||||
|
public function __get($name) |
||||
|
{ |
||||
|
if ($name === '_mh') { |
||||
|
return $this->_mh = curl_multi_init(); |
||||
|
} |
||||
|
|
||||
|
throw new \BadMethodCallException(); |
||||
|
} |
||||
|
|
||||
|
public function __destruct() |
||||
|
{ |
||||
|
if (isset($this->_mh)) { |
||||
|
curl_multi_close($this->_mh); |
||||
|
unset($this->_mh); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function __invoke(RequestInterface $request, array $options) |
||||
|
{ |
||||
|
$easy = $this->factory->create($request, $options); |
||||
|
$id = (int) $easy->handle; |
||||
|
|
||||
|
$promise = new Promise( |
||||
|
[$this, 'execute'], |
||||
|
function () use ($id) { |
||||
|
return $this->cancel($id); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
$this->addRequest(['easy' => $easy, 'deferred' => $promise]); |
||||
|
|
||||
|
return $promise; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Ticks the curl event loop. |
||||
|
*/ |
||||
|
public function tick() |
||||
|
{ |
||||
|
// Add any delayed handles if needed. |
||||
|
if ($this->delays) { |
||||
|
$currentTime = microtime(true); |
||||
|
foreach ($this->delays as $id => $delay) { |
||||
|
if ($currentTime >= $delay) { |
||||
|
unset($this->delays[$id]); |
||||
|
curl_multi_add_handle( |
||||
|
$this->_mh, |
||||
|
$this->handles[$id]['easy']->handle |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Step through the task queue which may add additional requests. |
||||
|
P\queue()->run(); |
||||
|
|
||||
|
if ($this->active && |
||||
|
curl_multi_select($this->_mh, $this->selectTimeout) === -1 |
||||
|
) { |
||||
|
// Perform a usleep if a select returns -1. |
||||
|
// See: https://bugs.php.net/bug.php?id=61141 |
||||
|
usleep(250); |
||||
|
} |
||||
|
|
||||
|
while (curl_multi_exec($this->_mh, $this->active) === CURLM_CALL_MULTI_PERFORM); |
||||
|
|
||||
|
$this->processMessages(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Runs until all outstanding connections have completed. |
||||
|
*/ |
||||
|
public function execute() |
||||
|
{ |
||||
|
$queue = P\queue(); |
||||
|
|
||||
|
while ($this->handles || !$queue->isEmpty()) { |
||||
|
// If there are no transfers, then sleep for the next delay |
||||
|
if (!$this->active && $this->delays) { |
||||
|
usleep($this->timeToNext()); |
||||
|
} |
||||
|
$this->tick(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function addRequest(array $entry) |
||||
|
{ |
||||
|
$easy = $entry['easy']; |
||||
|
$id = (int) $easy->handle; |
||||
|
$this->handles[$id] = $entry; |
||||
|
if (empty($easy->options['delay'])) { |
||||
|
curl_multi_add_handle($this->_mh, $easy->handle); |
||||
|
} else { |
||||
|
$this->delays[$id] = microtime(true) + ($easy->options['delay'] / 1000); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Cancels a handle from sending and removes references to it. |
||||
|
* |
||||
|
* @param int $id Handle ID to cancel and remove. |
||||
|
* |
||||
|
* @return bool True on success, false on failure. |
||||
|
*/ |
||||
|
private function cancel($id) |
||||
|
{ |
||||
|
// Cannot cancel if it has been processed. |
||||
|
if (!isset($this->handles[$id])) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
$handle = $this->handles[$id]['easy']->handle; |
||||
|
unset($this->delays[$id], $this->handles[$id]); |
||||
|
curl_multi_remove_handle($this->_mh, $handle); |
||||
|
curl_close($handle); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private function processMessages() |
||||
|
{ |
||||
|
while ($done = curl_multi_info_read($this->_mh)) { |
||||
|
$id = (int) $done['handle']; |
||||
|
curl_multi_remove_handle($this->_mh, $done['handle']); |
||||
|
|
||||
|
if (!isset($this->handles[$id])) { |
||||
|
// Probably was cancelled. |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
$entry = $this->handles[$id]; |
||||
|
unset($this->handles[$id], $this->delays[$id]); |
||||
|
$entry['easy']->errno = $done['result']; |
||||
|
$entry['deferred']->resolve( |
||||
|
CurlFactory::finish( |
||||
|
$this, |
||||
|
$entry['easy'], |
||||
|
$this->factory |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function timeToNext() |
||||
|
{ |
||||
|
$currentTime = microtime(true); |
||||
|
$nextTime = PHP_INT_MAX; |
||||
|
foreach ($this->delays as $time) { |
||||
|
if ($time < $nextTime) { |
||||
|
$nextTime = $time; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return max(0, $nextTime - $currentTime) * 1000000; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Handler; |
||||
|
|
||||
|
use GuzzleHttp\Psr7\Response; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
use Psr\Http\Message\StreamInterface; |
||||
|
|
||||
|
/** |
||||
|
* Represents a cURL easy handle and the data it populates. |
||||
|
* |
||||
|
* @internal |
||||
|
*/ |
||||
|
final class EasyHandle |
||||
|
{ |
||||
|
/** @var resource cURL resource */ |
||||
|
public $handle; |
||||
|
|
||||
|
/** @var StreamInterface Where data is being written */ |
||||
|
public $sink; |
||||
|
|
||||
|
/** @var array Received HTTP headers so far */ |
||||
|
public $headers = []; |
||||
|
|
||||
|
/** @var ResponseInterface Received response (if any) */ |
||||
|
public $response; |
||||
|
|
||||
|
/** @var RequestInterface Request being sent */ |
||||
|
public $request; |
||||
|
|
||||
|
/** @var array Request options */ |
||||
|
public $options = []; |
||||
|
|
||||
|
/** @var int cURL error number (if any) */ |
||||
|
public $errno = 0; |
||||
|
|
||||
|
/** @var \Exception Exception during on_headers (if any) */ |
||||
|
public $onHeadersException; |
||||
|
|
||||
|
/** |
||||
|
* Attach a response to the easy handle based on the received headers. |
||||
|
* |
||||
|
* @throws \RuntimeException if no headers have been received. |
||||
|
*/ |
||||
|
public function createResponse() |
||||
|
{ |
||||
|
if (empty($this->headers)) { |
||||
|
throw new \RuntimeException('No headers have been received'); |
||||
|
} |
||||
|
|
||||
|
// HTTP-version SP status-code SP reason-phrase |
||||
|
$startLine = explode(' ', array_shift($this->headers), 3); |
||||
|
$headers = \GuzzleHttp\headers_from_lines($this->headers); |
||||
|
$normalizedKeys = \GuzzleHttp\normalize_header_keys($headers); |
||||
|
|
||||
|
if (!empty($this->options['decode_content']) |
||||
|
&& isset($normalizedKeys['content-encoding']) |
||||
|
) { |
||||
|
$headers['x-encoded-content-encoding'] |
||||
|
= $headers[$normalizedKeys['content-encoding']]; |
||||
|
unset($headers[$normalizedKeys['content-encoding']]); |
||||
|
if (isset($normalizedKeys['content-length'])) { |
||||
|
$headers['x-encoded-content-length'] |
||||
|
= $headers[$normalizedKeys['content-length']]; |
||||
|
|
||||
|
$bodyLength = (int) $this->sink->getSize(); |
||||
|
if ($bodyLength) { |
||||
|
$headers[$normalizedKeys['content-length']] = $bodyLength; |
||||
|
} else { |
||||
|
unset($headers[$normalizedKeys['content-length']]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Attach a response to the easy handle with the parsed headers. |
||||
|
$this->response = new Response( |
||||
|
$startLine[1], |
||||
|
$headers, |
||||
|
$this->sink, |
||||
|
substr($startLine[0], 5), |
||||
|
isset($startLine[2]) ? (string) $startLine[2] : null |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public function __get($name) |
||||
|
{ |
||||
|
$msg = $name === 'handle' |
||||
|
? 'The EasyHandle has been released' |
||||
|
: 'Invalid property: ' . $name; |
||||
|
throw new \BadMethodCallException($msg); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,189 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Handler; |
||||
|
|
||||
|
use GuzzleHttp\Exception\RequestException; |
||||
|
use GuzzleHttp\HandlerStack; |
||||
|
use GuzzleHttp\Promise\PromiseInterface; |
||||
|
use GuzzleHttp\Promise\RejectedPromise; |
||||
|
use GuzzleHttp\TransferStats; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
|
||||
|
/** |
||||
|
* Handler that returns responses or throw exceptions from a queue. |
||||
|
*/ |
||||
|
class MockHandler implements \Countable |
||||
|
{ |
||||
|
private $queue = []; |
||||
|
private $lastRequest; |
||||
|
private $lastOptions; |
||||
|
private $onFulfilled; |
||||
|
private $onRejected; |
||||
|
|
||||
|
/** |
||||
|
* Creates a new MockHandler that uses the default handler stack list of |
||||
|
* middlewares. |
||||
|
* |
||||
|
* @param array $queue Array of responses, callables, or exceptions. |
||||
|
* @param callable $onFulfilled Callback to invoke when the return value is fulfilled. |
||||
|
* @param callable $onRejected Callback to invoke when the return value is rejected. |
||||
|
* |
||||
|
* @return HandlerStack |
||||
|
*/ |
||||
|
public static function createWithMiddleware( |
||||
|
array $queue = null, |
||||
|
callable $onFulfilled = null, |
||||
|
callable $onRejected = null |
||||
|
) { |
||||
|
return HandlerStack::create(new self($queue, $onFulfilled, $onRejected)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* The passed in value must be an array of |
||||
|
* {@see Psr7\Http\Message\ResponseInterface} objects, Exceptions, |
||||
|
* callables, or Promises. |
||||
|
* |
||||
|
* @param array $queue |
||||
|
* @param callable $onFulfilled Callback to invoke when the return value is fulfilled. |
||||
|
* @param callable $onRejected Callback to invoke when the return value is rejected. |
||||
|
*/ |
||||
|
public function __construct( |
||||
|
array $queue = null, |
||||
|
callable $onFulfilled = null, |
||||
|
callable $onRejected = null |
||||
|
) { |
||||
|
$this->onFulfilled = $onFulfilled; |
||||
|
$this->onRejected = $onRejected; |
||||
|
|
||||
|
if ($queue) { |
||||
|
call_user_func_array([$this, 'append'], $queue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function __invoke(RequestInterface $request, array $options) |
||||
|
{ |
||||
|
if (!$this->queue) { |
||||
|
throw new \OutOfBoundsException('Mock queue is empty'); |
||||
|
} |
||||
|
|
||||
|
if (isset($options['delay'])) { |
||||
|
usleep($options['delay'] * 1000); |
||||
|
} |
||||
|
|
||||
|
$this->lastRequest = $request; |
||||
|
$this->lastOptions = $options; |
||||
|
$response = array_shift($this->queue); |
||||
|
|
||||
|
if (isset($options['on_headers'])) { |
||||
|
if (!is_callable($options['on_headers'])) { |
||||
|
throw new \InvalidArgumentException('on_headers must be callable'); |
||||
|
} |
||||
|
try { |
||||
|
$options['on_headers']($response); |
||||
|
} catch (\Exception $e) { |
||||
|
$msg = 'An error was encountered during the on_headers event'; |
||||
|
$response = new RequestException($msg, $request, $response, $e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (is_callable($response)) { |
||||
|
$response = call_user_func($response, $request, $options); |
||||
|
} |
||||
|
|
||||
|
$response = $response instanceof \Exception |
||||
|
? \GuzzleHttp\Promise\rejection_for($response) |
||||
|
: \GuzzleHttp\Promise\promise_for($response); |
||||
|
|
||||
|
return $response->then( |
||||
|
function ($value) use ($request, $options) { |
||||
|
$this->invokeStats($request, $options, $value); |
||||
|
if ($this->onFulfilled) { |
||||
|
call_user_func($this->onFulfilled, $value); |
||||
|
} |
||||
|
if (isset($options['sink'])) { |
||||
|
$contents = (string) $value->getBody(); |
||||
|
$sink = $options['sink']; |
||||
|
|
||||
|
if (is_resource($sink)) { |
||||
|
fwrite($sink, $contents); |
||||
|
} elseif (is_string($sink)) { |
||||
|
file_put_contents($sink, $contents); |
||||
|
} elseif ($sink instanceof \Psr\Http\Message\StreamInterface) { |
||||
|
$sink->write($contents); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $value; |
||||
|
}, |
||||
|
function ($reason) use ($request, $options) { |
||||
|
$this->invokeStats($request, $options, null, $reason); |
||||
|
if ($this->onRejected) { |
||||
|
call_user_func($this->onRejected, $reason); |
||||
|
} |
||||
|
return \GuzzleHttp\Promise\rejection_for($reason); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Adds one or more variadic requests, exceptions, callables, or promises |
||||
|
* to the queue. |
||||
|
*/ |
||||
|
public function append() |
||||
|
{ |
||||
|
foreach (func_get_args() as $value) { |
||||
|
if ($value instanceof ResponseInterface |
||||
|
|| $value instanceof \Exception |
||||
|
|| $value instanceof PromiseInterface |
||||
|
|| is_callable($value) |
||||
|
) { |
||||
|
$this->queue[] = $value; |
||||
|
} else { |
||||
|
throw new \InvalidArgumentException('Expected a response or ' |
||||
|
. 'exception. Found ' . \GuzzleHttp\describe_type($value)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the last received request. |
||||
|
* |
||||
|
* @return RequestInterface |
||||
|
*/ |
||||
|
public function getLastRequest() |
||||
|
{ |
||||
|
return $this->lastRequest; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the last received request options. |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function getLastOptions() |
||||
|
{ |
||||
|
return $this->lastOptions; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the number of remaining items in the queue. |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public function count() |
||||
|
{ |
||||
|
return count($this->queue); |
||||
|
} |
||||
|
|
||||
|
private function invokeStats( |
||||
|
RequestInterface $request, |
||||
|
array $options, |
||||
|
ResponseInterface $response = null, |
||||
|
$reason = null |
||||
|
) { |
||||
|
if (isset($options['on_stats'])) { |
||||
|
$stats = new TransferStats($request, $response, 0, $reason); |
||||
|
call_user_func($options['on_stats'], $stats); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Handler; |
||||
|
|
||||
|
use GuzzleHttp\RequestOptions; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
|
||||
|
/** |
||||
|
* Provides basic proxies for handlers. |
||||
|
*/ |
||||
|
class Proxy |
||||
|
{ |
||||
|
/** |
||||
|
* Sends synchronous requests to a specific handler while sending all other |
||||
|
* requests to another handler. |
||||
|
* |
||||
|
* @param callable $default Handler used for normal responses |
||||
|
* @param callable $sync Handler used for synchronous responses. |
||||
|
* |
||||
|
* @return callable Returns the composed handler. |
||||
|
*/ |
||||
|
public static function wrapSync( |
||||
|
callable $default, |
||||
|
callable $sync |
||||
|
) { |
||||
|
return function (RequestInterface $request, array $options) use ($default, $sync) { |
||||
|
return empty($options[RequestOptions::SYNCHRONOUS]) |
||||
|
? $default($request, $options) |
||||
|
: $sync($request, $options); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Sends streaming requests to a streaming compatible handler while sending |
||||
|
* all other requests to a default handler. |
||||
|
* |
||||
|
* This, for example, could be useful for taking advantage of the |
||||
|
* performance benefits of curl while still supporting true streaming |
||||
|
* through the StreamHandler. |
||||
|
* |
||||
|
* @param callable $default Handler used for non-streaming responses |
||||
|
* @param callable $streaming Handler used for streaming responses |
||||
|
* |
||||
|
* @return callable Returns the composed handler. |
||||
|
*/ |
||||
|
public static function wrapStreaming( |
||||
|
callable $default, |
||||
|
callable $streaming |
||||
|
) { |
||||
|
return function (RequestInterface $request, array $options) use ($default, $streaming) { |
||||
|
return empty($options['stream']) |
||||
|
? $default($request, $options) |
||||
|
: $streaming($request, $options); |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,532 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Handler; |
||||
|
|
||||
|
use GuzzleHttp\Exception\RequestException; |
||||
|
use GuzzleHttp\Exception\ConnectException; |
||||
|
use GuzzleHttp\Promise\FulfilledPromise; |
||||
|
use GuzzleHttp\Promise\PromiseInterface; |
||||
|
use GuzzleHttp\Psr7; |
||||
|
use GuzzleHttp\TransferStats; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
use Psr\Http\Message\StreamInterface; |
||||
|
|
||||
|
/** |
||||
|
* HTTP handler that uses PHP's HTTP stream wrapper. |
||||
|
*/ |
||||
|
class StreamHandler |
||||
|
{ |
||||
|
private $lastHeaders = []; |
||||
|
|
||||
|
/** |
||||
|
* Sends an HTTP request. |
||||
|
* |
||||
|
* @param RequestInterface $request Request to send. |
||||
|
* @param array $options Request transfer options. |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
public function __invoke(RequestInterface $request, array $options) |
||||
|
{ |
||||
|
// Sleep if there is a delay specified. |
||||
|
if (isset($options['delay'])) { |
||||
|
usleep($options['delay'] * 1000); |
||||
|
} |
||||
|
|
||||
|
$startTime = isset($options['on_stats']) ? microtime(true) : null; |
||||
|
|
||||
|
try { |
||||
|
// Does not support the expect header. |
||||
|
$request = $request->withoutHeader('Expect'); |
||||
|
|
||||
|
// Append a content-length header if body size is zero to match |
||||
|
// cURL's behavior. |
||||
|
if (0 === $request->getBody()->getSize()) { |
||||
|
$request = $request->withHeader('Content-Length', 0); |
||||
|
} |
||||
|
|
||||
|
return $this->createResponse( |
||||
|
$request, |
||||
|
$options, |
||||
|
$this->createStream($request, $options), |
||||
|
$startTime |
||||
|
); |
||||
|
} catch (\InvalidArgumentException $e) { |
||||
|
throw $e; |
||||
|
} catch (\Exception $e) { |
||||
|
// Determine if the error was a networking error. |
||||
|
$message = $e->getMessage(); |
||||
|
// This list can probably get more comprehensive. |
||||
|
if (strpos($message, 'getaddrinfo') // DNS lookup failed |
||||
|
|| strpos($message, 'Connection refused') |
||||
|
|| strpos($message, "couldn't connect to host") // error on HHVM |
||||
|
|| strpos($message, "connection attempt failed") |
||||
|
) { |
||||
|
$e = new ConnectException($e->getMessage(), $request, $e); |
||||
|
} |
||||
|
$e = RequestException::wrapException($request, $e); |
||||
|
$this->invokeStats($options, $request, $startTime, null, $e); |
||||
|
|
||||
|
return \GuzzleHttp\Promise\rejection_for($e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function invokeStats( |
||||
|
array $options, |
||||
|
RequestInterface $request, |
||||
|
$startTime, |
||||
|
ResponseInterface $response = null, |
||||
|
$error = null |
||||
|
) { |
||||
|
if (isset($options['on_stats'])) { |
||||
|
$stats = new TransferStats( |
||||
|
$request, |
||||
|
$response, |
||||
|
microtime(true) - $startTime, |
||||
|
$error, |
||||
|
[] |
||||
|
); |
||||
|
call_user_func($options['on_stats'], $stats); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function createResponse( |
||||
|
RequestInterface $request, |
||||
|
array $options, |
||||
|
$stream, |
||||
|
$startTime |
||||
|
) { |
||||
|
$hdrs = $this->lastHeaders; |
||||
|
$this->lastHeaders = []; |
||||
|
$parts = explode(' ', array_shift($hdrs), 3); |
||||
|
$ver = explode('/', $parts[0])[1]; |
||||
|
$status = $parts[1]; |
||||
|
$reason = isset($parts[2]) ? $parts[2] : null; |
||||
|
$headers = \GuzzleHttp\headers_from_lines($hdrs); |
||||
|
list($stream, $headers) = $this->checkDecode($options, $headers, $stream); |
||||
|
$stream = Psr7\stream_for($stream); |
||||
|
$sink = $stream; |
||||
|
|
||||
|
if (strcasecmp('HEAD', $request->getMethod())) { |
||||
|
$sink = $this->createSink($stream, $options); |
||||
|
} |
||||
|
|
||||
|
$response = new Psr7\Response($status, $headers, $sink, $ver, $reason); |
||||
|
|
||||
|
if (isset($options['on_headers'])) { |
||||
|
try { |
||||
|
$options['on_headers']($response); |
||||
|
} catch (\Exception $e) { |
||||
|
$msg = 'An error was encountered during the on_headers event'; |
||||
|
$ex = new RequestException($msg, $request, $response, $e); |
||||
|
return \GuzzleHttp\Promise\rejection_for($ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Do not drain when the request is a HEAD request because they have |
||||
|
// no body. |
||||
|
if ($sink !== $stream) { |
||||
|
$this->drain( |
||||
|
$stream, |
||||
|
$sink, |
||||
|
$response->getHeaderLine('Content-Length') |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
$this->invokeStats($options, $request, $startTime, $response, null); |
||||
|
|
||||
|
return new FulfilledPromise($response); |
||||
|
} |
||||
|
|
||||
|
private function createSink(StreamInterface $stream, array $options) |
||||
|
{ |
||||
|
if (!empty($options['stream'])) { |
||||
|
return $stream; |
||||
|
} |
||||
|
|
||||
|
$sink = isset($options['sink']) |
||||
|
? $options['sink'] |
||||
|
: fopen('php://temp', 'r+'); |
||||
|
|
||||
|
return is_string($sink) |
||||
|
? new Psr7\LazyOpenStream($sink, 'w+') |
||||
|
: Psr7\stream_for($sink); |
||||
|
} |
||||
|
|
||||
|
private function checkDecode(array $options, array $headers, $stream) |
||||
|
{ |
||||
|
// Automatically decode responses when instructed. |
||||
|
if (!empty($options['decode_content'])) { |
||||
|
$normalizedKeys = \GuzzleHttp\normalize_header_keys($headers); |
||||
|
if (isset($normalizedKeys['content-encoding'])) { |
||||
|
$encoding = $headers[$normalizedKeys['content-encoding']]; |
||||
|
if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') { |
||||
|
$stream = new Psr7\InflateStream( |
||||
|
Psr7\stream_for($stream) |
||||
|
); |
||||
|
$headers['x-encoded-content-encoding'] |
||||
|
= $headers[$normalizedKeys['content-encoding']]; |
||||
|
// Remove content-encoding header |
||||
|
unset($headers[$normalizedKeys['content-encoding']]); |
||||
|
// Fix content-length header |
||||
|
if (isset($normalizedKeys['content-length'])) { |
||||
|
$headers['x-encoded-content-length'] |
||||
|
= $headers[$normalizedKeys['content-length']]; |
||||
|
|
||||
|
$length = (int) $stream->getSize(); |
||||
|
if ($length === 0) { |
||||
|
unset($headers[$normalizedKeys['content-length']]); |
||||
|
} else { |
||||
|
$headers[$normalizedKeys['content-length']] = [$length]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return [$stream, $headers]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Drains the source stream into the "sink" client option. |
||||
|
* |
||||
|
* @param StreamInterface $source |
||||
|
* @param StreamInterface $sink |
||||
|
* @param string $contentLength Header specifying the amount of |
||||
|
* data to read. |
||||
|
* |
||||
|
* @return StreamInterface |
||||
|
* @throws \RuntimeException when the sink option is invalid. |
||||
|
*/ |
||||
|
private function drain( |
||||
|
StreamInterface $source, |
||||
|
StreamInterface $sink, |
||||
|
$contentLength |
||||
|
) { |
||||
|
// If a content-length header is provided, then stop reading once |
||||
|
// that number of bytes has been read. This can prevent infinitely |
||||
|
// reading from a stream when dealing with servers that do not honor |
||||
|
// Connection: Close headers. |
||||
|
Psr7\copy_to_stream( |
||||
|
$source, |
||||
|
$sink, |
||||
|
(strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1 |
||||
|
); |
||||
|
|
||||
|
$sink->seek(0); |
||||
|
$source->close(); |
||||
|
|
||||
|
return $sink; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Create a resource and check to ensure it was created successfully |
||||
|
* |
||||
|
* @param callable $callback Callable that returns stream resource |
||||
|
* |
||||
|
* @return resource |
||||
|
* @throws \RuntimeException on error |
||||
|
*/ |
||||
|
private function createResource(callable $callback) |
||||
|
{ |
||||
|
$errors = null; |
||||
|
set_error_handler(function ($_, $msg, $file, $line) use (&$errors) { |
||||
|
$errors[] = [ |
||||
|
'message' => $msg, |
||||
|
'file' => $file, |
||||
|
'line' => $line |
||||
|
]; |
||||
|
return true; |
||||
|
}); |
||||
|
|
||||
|
$resource = $callback(); |
||||
|
restore_error_handler(); |
||||
|
|
||||
|
if (!$resource) { |
||||
|
$message = 'Error creating resource: '; |
||||
|
foreach ($errors as $err) { |
||||
|
foreach ($err as $key => $value) { |
||||
|
$message .= "[$key] $value" . PHP_EOL; |
||||
|
} |
||||
|
} |
||||
|
throw new \RuntimeException(trim($message)); |
||||
|
} |
||||
|
|
||||
|
return $resource; |
||||
|
} |
||||
|
|
||||
|
private function createStream(RequestInterface $request, array $options) |
||||
|
{ |
||||
|
static $methods; |
||||
|
if (!$methods) { |
||||
|
$methods = array_flip(get_class_methods(__CLASS__)); |
||||
|
} |
||||
|
|
||||
|
// HTTP/1.1 streams using the PHP stream wrapper require a |
||||
|
// Connection: close header |
||||
|
if ($request->getProtocolVersion() == '1.1' |
||||
|
&& !$request->hasHeader('Connection') |
||||
|
) { |
||||
|
$request = $request->withHeader('Connection', 'close'); |
||||
|
} |
||||
|
|
||||
|
// Ensure SSL is verified by default |
||||
|
if (!isset($options['verify'])) { |
||||
|
$options['verify'] = true; |
||||
|
} |
||||
|
|
||||
|
$params = []; |
||||
|
$context = $this->getDefaultContext($request); |
||||
|
|
||||
|
if (isset($options['on_headers']) && !is_callable($options['on_headers'])) { |
||||
|
throw new \InvalidArgumentException('on_headers must be callable'); |
||||
|
} |
||||
|
|
||||
|
if (!empty($options)) { |
||||
|
foreach ($options as $key => $value) { |
||||
|
$method = "add_{$key}"; |
||||
|
if (isset($methods[$method])) { |
||||
|
$this->{$method}($request, $context, $value, $params); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (isset($options['stream_context'])) { |
||||
|
if (!is_array($options['stream_context'])) { |
||||
|
throw new \InvalidArgumentException('stream_context must be an array'); |
||||
|
} |
||||
|
$context = array_replace_recursive( |
||||
|
$context, |
||||
|
$options['stream_context'] |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// Microsoft NTLM authentication only supported with curl handler |
||||
|
if (isset($options['auth']) |
||||
|
&& is_array($options['auth']) |
||||
|
&& isset($options['auth'][2]) |
||||
|
&& 'ntlm' == $options['auth'][2] |
||||
|
) { |
||||
|
throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler'); |
||||
|
} |
||||
|
|
||||
|
$uri = $this->resolveHost($request, $options); |
||||
|
|
||||
|
$context = $this->createResource( |
||||
|
function () use ($context, $params) { |
||||
|
return stream_context_create($context, $params); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
return $this->createResource( |
||||
|
function () use ($uri, &$http_response_header, $context, $options) { |
||||
|
$resource = fopen((string) $uri, 'r', null, $context); |
||||
|
$this->lastHeaders = $http_response_header; |
||||
|
|
||||
|
if (isset($options['read_timeout'])) { |
||||
|
$readTimeout = $options['read_timeout']; |
||||
|
$sec = (int) $readTimeout; |
||||
|
$usec = ($readTimeout - $sec) * 100000; |
||||
|
stream_set_timeout($resource, $sec, $usec); |
||||
|
} |
||||
|
|
||||
|
return $resource; |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private function resolveHost(RequestInterface $request, array $options) |
||||
|
{ |
||||
|
$uri = $request->getUri(); |
||||
|
|
||||
|
if (isset($options['force_ip_resolve']) && !filter_var($uri->getHost(), FILTER_VALIDATE_IP)) { |
||||
|
if ('v4' === $options['force_ip_resolve']) { |
||||
|
$records = dns_get_record($uri->getHost(), DNS_A); |
||||
|
if (!isset($records[0]['ip'])) { |
||||
|
throw new ConnectException(sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request); |
||||
|
} |
||||
|
$uri = $uri->withHost($records[0]['ip']); |
||||
|
} elseif ('v6' === $options['force_ip_resolve']) { |
||||
|
$records = dns_get_record($uri->getHost(), DNS_AAAA); |
||||
|
if (!isset($records[0]['ipv6'])) { |
||||
|
throw new ConnectException(sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request); |
||||
|
} |
||||
|
$uri = $uri->withHost('[' . $records[0]['ipv6'] . ']'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $uri; |
||||
|
} |
||||
|
|
||||
|
private function getDefaultContext(RequestInterface $request) |
||||
|
{ |
||||
|
$headers = ''; |
||||
|
foreach ($request->getHeaders() as $name => $value) { |
||||
|
foreach ($value as $val) { |
||||
|
$headers .= "$name: $val\r\n"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$context = [ |
||||
|
'http' => [ |
||||
|
'method' => $request->getMethod(), |
||||
|
'header' => $headers, |
||||
|
'protocol_version' => $request->getProtocolVersion(), |
||||
|
'ignore_errors' => true, |
||||
|
'follow_location' => 0, |
||||
|
], |
||||
|
]; |
||||
|
|
||||
|
$body = (string) $request->getBody(); |
||||
|
|
||||
|
if (!empty($body)) { |
||||
|
$context['http']['content'] = $body; |
||||
|
// Prevent the HTTP handler from adding a Content-Type header. |
||||
|
if (!$request->hasHeader('Content-Type')) { |
||||
|
$context['http']['header'] .= "Content-Type:\r\n"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$context['http']['header'] = rtrim($context['http']['header']); |
||||
|
|
||||
|
return $context; |
||||
|
} |
||||
|
|
||||
|
private function add_proxy(RequestInterface $request, &$options, $value, &$params) |
||||
|
{ |
||||
|
if (!is_array($value)) { |
||||
|
$options['http']['proxy'] = $value; |
||||
|
} else { |
||||
|
$scheme = $request->getUri()->getScheme(); |
||||
|
if (isset($value[$scheme])) { |
||||
|
if (!isset($value['no']) |
||||
|
|| !\GuzzleHttp\is_host_in_noproxy( |
||||
|
$request->getUri()->getHost(), |
||||
|
$value['no'] |
||||
|
) |
||||
|
) { |
||||
|
$options['http']['proxy'] = $value[$scheme]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function add_timeout(RequestInterface $request, &$options, $value, &$params) |
||||
|
{ |
||||
|
if ($value > 0) { |
||||
|
$options['http']['timeout'] = $value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function add_verify(RequestInterface $request, &$options, $value, &$params) |
||||
|
{ |
||||
|
if ($value === true) { |
||||
|
// PHP 5.6 or greater will find the system cert by default. When |
||||
|
// < 5.6, use the Guzzle bundled cacert. |
||||
|
if (PHP_VERSION_ID < 50600) { |
||||
|
$options['ssl']['cafile'] = \GuzzleHttp\default_ca_bundle(); |
||||
|
} |
||||
|
} elseif (is_string($value)) { |
||||
|
$options['ssl']['cafile'] = $value; |
||||
|
if (!file_exists($value)) { |
||||
|
throw new \RuntimeException("SSL CA bundle not found: $value"); |
||||
|
} |
||||
|
} elseif ($value === false) { |
||||
|
$options['ssl']['verify_peer'] = false; |
||||
|
$options['ssl']['verify_peer_name'] = false; |
||||
|
return; |
||||
|
} else { |
||||
|
throw new \InvalidArgumentException('Invalid verify request option'); |
||||
|
} |
||||
|
|
||||
|
$options['ssl']['verify_peer'] = true; |
||||
|
$options['ssl']['verify_peer_name'] = true; |
||||
|
$options['ssl']['allow_self_signed'] = false; |
||||
|
} |
||||
|
|
||||
|
private function add_cert(RequestInterface $request, &$options, $value, &$params) |
||||
|
{ |
||||
|
if (is_array($value)) { |
||||
|
$options['ssl']['passphrase'] = $value[1]; |
||||
|
$value = $value[0]; |
||||
|
} |
||||
|
|
||||
|
if (!file_exists($value)) { |
||||
|
throw new \RuntimeException("SSL certificate not found: {$value}"); |
||||
|
} |
||||
|
|
||||
|
$options['ssl']['local_cert'] = $value; |
||||
|
} |
||||
|
|
||||
|
private function add_progress(RequestInterface $request, &$options, $value, &$params) |
||||
|
{ |
||||
|
$this->addNotification( |
||||
|
$params, |
||||
|
function ($code, $a, $b, $c, $transferred, $total) use ($value) { |
||||
|
if ($code == STREAM_NOTIFY_PROGRESS) { |
||||
|
$value($total, $transferred, null, null); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private function add_debug(RequestInterface $request, &$options, $value, &$params) |
||||
|
{ |
||||
|
if ($value === false) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
static $map = [ |
||||
|
STREAM_NOTIFY_CONNECT => 'CONNECT', |
||||
|
STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', |
||||
|
STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', |
||||
|
STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', |
||||
|
STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', |
||||
|
STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', |
||||
|
STREAM_NOTIFY_PROGRESS => 'PROGRESS', |
||||
|
STREAM_NOTIFY_FAILURE => 'FAILURE', |
||||
|
STREAM_NOTIFY_COMPLETED => 'COMPLETED', |
||||
|
STREAM_NOTIFY_RESOLVE => 'RESOLVE', |
||||
|
]; |
||||
|
static $args = ['severity', 'message', 'message_code', |
||||
|
'bytes_transferred', 'bytes_max']; |
||||
|
|
||||
|
$value = \GuzzleHttp\debug_resource($value); |
||||
|
$ident = $request->getMethod() . ' ' . $request->getUri()->withFragment(''); |
||||
|
$this->addNotification( |
||||
|
$params, |
||||
|
function () use ($ident, $value, $map, $args) { |
||||
|
$passed = func_get_args(); |
||||
|
$code = array_shift($passed); |
||||
|
fprintf($value, '<%s> [%s] ', $ident, $map[$code]); |
||||
|
foreach (array_filter($passed) as $i => $v) { |
||||
|
fwrite($value, $args[$i] . ': "' . $v . '" '); |
||||
|
} |
||||
|
fwrite($value, "\n"); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private function addNotification(array &$params, callable $notify) |
||||
|
{ |
||||
|
// Wrap the existing function if needed. |
||||
|
if (!isset($params['notification'])) { |
||||
|
$params['notification'] = $notify; |
||||
|
} else { |
||||
|
$params['notification'] = $this->callArray([ |
||||
|
$params['notification'], |
||||
|
$notify |
||||
|
]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function callArray(array $functions) |
||||
|
{ |
||||
|
return function () use ($functions) { |
||||
|
$args = func_get_args(); |
||||
|
foreach ($functions as $fn) { |
||||
|
call_user_func_array($fn, $args); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,273 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp; |
||||
|
|
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
|
||||
|
/** |
||||
|
* Creates a composed Guzzle handler function by stacking middlewares on top of |
||||
|
* an HTTP handler function. |
||||
|
*/ |
||||
|
class HandlerStack |
||||
|
{ |
||||
|
/** @var callable */ |
||||
|
private $handler; |
||||
|
|
||||
|
/** @var array */ |
||||
|
private $stack = []; |
||||
|
|
||||
|
/** @var callable|null */ |
||||
|
private $cached; |
||||
|
|
||||
|
/** |
||||
|
* Creates a default handler stack that can be used by clients. |
||||
|
* |
||||
|
* The returned handler will wrap the provided handler or use the most |
||||
|
* appropriate default handler for your system. The returned HandlerStack has |
||||
|
* support for cookies, redirects, HTTP error exceptions, and preparing a body |
||||
|
* before sending. |
||||
|
* |
||||
|
* The returned handler stack can be passed to a client in the "handler" |
||||
|
* option. |
||||
|
* |
||||
|
* @param callable $handler HTTP handler function to use with the stack. If no |
||||
|
* handler is provided, the best handler for your |
||||
|
* system will be utilized. |
||||
|
* |
||||
|
* @return HandlerStack |
||||
|
*/ |
||||
|
public static function create(callable $handler = null) |
||||
|
{ |
||||
|
$stack = new self($handler ?: choose_handler()); |
||||
|
$stack->push(Middleware::httpErrors(), 'http_errors'); |
||||
|
$stack->push(Middleware::redirect(), 'allow_redirects'); |
||||
|
$stack->push(Middleware::cookies(), 'cookies'); |
||||
|
$stack->push(Middleware::prepareBody(), 'prepare_body'); |
||||
|
|
||||
|
return $stack; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param callable $handler Underlying HTTP handler. |
||||
|
*/ |
||||
|
public function __construct(callable $handler = null) |
||||
|
{ |
||||
|
$this->handler = $handler; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Invokes the handler stack as a composed handler |
||||
|
* |
||||
|
* @param RequestInterface $request |
||||
|
* @param array $options |
||||
|
*/ |
||||
|
public function __invoke(RequestInterface $request, array $options) |
||||
|
{ |
||||
|
$handler = $this->resolve(); |
||||
|
|
||||
|
return $handler($request, $options); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Dumps a string representation of the stack. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function __toString() |
||||
|
{ |
||||
|
$depth = 0; |
||||
|
$stack = []; |
||||
|
if ($this->handler) { |
||||
|
$stack[] = "0) Handler: " . $this->debugCallable($this->handler); |
||||
|
} |
||||
|
|
||||
|
$result = ''; |
||||
|
foreach (array_reverse($this->stack) as $tuple) { |
||||
|
$depth++; |
||||
|
$str = "{$depth}) Name: '{$tuple[1]}', "; |
||||
|
$str .= "Function: " . $this->debugCallable($tuple[0]); |
||||
|
$result = "> {$str}\n{$result}"; |
||||
|
$stack[] = $str; |
||||
|
} |
||||
|
|
||||
|
foreach (array_keys($stack) as $k) { |
||||
|
$result .= "< {$stack[$k]}\n"; |
||||
|
} |
||||
|
|
||||
|
return $result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the HTTP handler that actually returns a promise. |
||||
|
* |
||||
|
* @param callable $handler Accepts a request and array of options and |
||||
|
* returns a Promise. |
||||
|
*/ |
||||
|
public function setHandler(callable $handler) |
||||
|
{ |
||||
|
$this->handler = $handler; |
||||
|
$this->cached = null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns true if the builder has a handler. |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function hasHandler() |
||||
|
{ |
||||
|
return (bool) $this->handler; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Unshift a middleware to the bottom of the stack. |
||||
|
* |
||||
|
* @param callable $middleware Middleware function |
||||
|
* @param string $name Name to register for this middleware. |
||||
|
*/ |
||||
|
public function unshift(callable $middleware, $name = null) |
||||
|
{ |
||||
|
array_unshift($this->stack, [$middleware, $name]); |
||||
|
$this->cached = null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Push a middleware to the top of the stack. |
||||
|
* |
||||
|
* @param callable $middleware Middleware function |
||||
|
* @param string $name Name to register for this middleware. |
||||
|
*/ |
||||
|
public function push(callable $middleware, $name = '') |
||||
|
{ |
||||
|
$this->stack[] = [$middleware, $name]; |
||||
|
$this->cached = null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Add a middleware before another middleware by name. |
||||
|
* |
||||
|
* @param string $findName Middleware to find |
||||
|
* @param callable $middleware Middleware function |
||||
|
* @param string $withName Name to register for this middleware. |
||||
|
*/ |
||||
|
public function before($findName, callable $middleware, $withName = '') |
||||
|
{ |
||||
|
$this->splice($findName, $withName, $middleware, true); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Add a middleware after another middleware by name. |
||||
|
* |
||||
|
* @param string $findName Middleware to find |
||||
|
* @param callable $middleware Middleware function |
||||
|
* @param string $withName Name to register for this middleware. |
||||
|
*/ |
||||
|
public function after($findName, callable $middleware, $withName = '') |
||||
|
{ |
||||
|
$this->splice($findName, $withName, $middleware, false); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Remove a middleware by instance or name from the stack. |
||||
|
* |
||||
|
* @param callable|string $remove Middleware to remove by instance or name. |
||||
|
*/ |
||||
|
public function remove($remove) |
||||
|
{ |
||||
|
$this->cached = null; |
||||
|
$idx = is_callable($remove) ? 0 : 1; |
||||
|
$this->stack = array_values(array_filter( |
||||
|
$this->stack, |
||||
|
function ($tuple) use ($idx, $remove) { |
||||
|
return $tuple[$idx] !== $remove; |
||||
|
} |
||||
|
)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Compose the middleware and handler into a single callable function. |
||||
|
* |
||||
|
* @return callable |
||||
|
*/ |
||||
|
public function resolve() |
||||
|
{ |
||||
|
if (!$this->cached) { |
||||
|
if (!($prev = $this->handler)) { |
||||
|
throw new \LogicException('No handler has been specified'); |
||||
|
} |
||||
|
|
||||
|
foreach (array_reverse($this->stack) as $fn) { |
||||
|
$prev = $fn[0]($prev); |
||||
|
} |
||||
|
|
||||
|
$this->cached = $prev; |
||||
|
} |
||||
|
|
||||
|
return $this->cached; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param $name |
||||
|
* @return int |
||||
|
*/ |
||||
|
private function findByName($name) |
||||
|
{ |
||||
|
foreach ($this->stack as $k => $v) { |
||||
|
if ($v[1] === $name) { |
||||
|
return $k; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
throw new \InvalidArgumentException("Middleware not found: $name"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Splices a function into the middleware list at a specific position. |
||||
|
* |
||||
|
* @param $findName |
||||
|
* @param $withName |
||||
|
* @param callable $middleware |
||||
|
* @param $before |
||||
|
*/ |
||||
|
private function splice($findName, $withName, callable $middleware, $before) |
||||
|
{ |
||||
|
$this->cached = null; |
||||
|
$idx = $this->findByName($findName); |
||||
|
$tuple = [$middleware, $withName]; |
||||
|
|
||||
|
if ($before) { |
||||
|
if ($idx === 0) { |
||||
|
array_unshift($this->stack, $tuple); |
||||
|
} else { |
||||
|
$replacement = [$tuple, $this->stack[$idx]]; |
||||
|
array_splice($this->stack, $idx, 1, $replacement); |
||||
|
} |
||||
|
} elseif ($idx === count($this->stack) - 1) { |
||||
|
$this->stack[] = $tuple; |
||||
|
} else { |
||||
|
$replacement = [$this->stack[$idx], $tuple]; |
||||
|
array_splice($this->stack, $idx, 1, $replacement); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Provides a debug string for a given callable. |
||||
|
* |
||||
|
* @param array|callable $fn Function to write as a string. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
private function debugCallable($fn) |
||||
|
{ |
||||
|
if (is_string($fn)) { |
||||
|
return "callable({$fn})"; |
||||
|
} |
||||
|
|
||||
|
if (is_array($fn)) { |
||||
|
return is_string($fn[0]) |
||||
|
? "callable({$fn[0]}::{$fn[1]})" |
||||
|
: "callable(['" . get_class($fn[0]) . "', '{$fn[1]}'])"; |
||||
|
} |
||||
|
|
||||
|
return 'callable(' . spl_object_hash($fn) . ')'; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,180 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp; |
||||
|
|
||||
|
use Psr\Http\Message\MessageInterface; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
|
||||
|
/** |
||||
|
* Formats log messages using variable substitutions for requests, responses, |
||||
|
* and other transactional data. |
||||
|
* |
||||
|
* The following variable substitutions are supported: |
||||
|
* |
||||
|
* - {request}: Full HTTP request message |
||||
|
* - {response}: Full HTTP response message |
||||
|
* - {ts}: ISO 8601 date in GMT |
||||
|
* - {date_iso_8601} ISO 8601 date in GMT |
||||
|
* - {date_common_log} Apache common log date using the configured timezone. |
||||
|
* - {host}: Host of the request |
||||
|
* - {method}: Method of the request |
||||
|
* - {uri}: URI of the request |
||||
|
* - {version}: Protocol version |
||||
|
* - {target}: Request target of the request (path + query + fragment) |
||||
|
* - {hostname}: Hostname of the machine that sent the request |
||||
|
* - {code}: Status code of the response (if available) |
||||
|
* - {phrase}: Reason phrase of the response (if available) |
||||
|
* - {error}: Any error messages (if available) |
||||
|
* - {req_header_*}: Replace `*` with the lowercased name of a request header to add to the message |
||||
|
* - {res_header_*}: Replace `*` with the lowercased name of a response header to add to the message |
||||
|
* - {req_headers}: Request headers |
||||
|
* - {res_headers}: Response headers |
||||
|
* - {req_body}: Request body |
||||
|
* - {res_body}: Response body |
||||
|
*/ |
||||
|
class MessageFormatter |
||||
|
{ |
||||
|
/** |
||||
|
* Apache Common Log Format. |
||||
|
* @link http://httpd.apache.org/docs/2.4/logs.html#common |
||||
|
* @var string |
||||
|
*/ |
||||
|
const CLF = "{hostname} {req_header_User-Agent} - [{date_common_log}] \"{method} {target} HTTP/{version}\" {code} {res_header_Content-Length}"; |
||||
|
const DEBUG = ">>>>>>>>\n{request}\n<<<<<<<<\n{response}\n--------\n{error}"; |
||||
|
const SHORT = '[{ts}] "{method} {target} HTTP/{version}" {code}'; |
||||
|
|
||||
|
/** @var string Template used to format log messages */ |
||||
|
private $template; |
||||
|
|
||||
|
/** |
||||
|
* @param string $template Log message template |
||||
|
*/ |
||||
|
public function __construct($template = self::CLF) |
||||
|
{ |
||||
|
$this->template = $template ?: self::CLF; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns a formatted message string. |
||||
|
* |
||||
|
* @param RequestInterface $request Request that was sent |
||||
|
* @param ResponseInterface $response Response that was received |
||||
|
* @param \Exception $error Exception that was received |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function format( |
||||
|
RequestInterface $request, |
||||
|
ResponseInterface $response = null, |
||||
|
\Exception $error = null |
||||
|
) { |
||||
|
$cache = []; |
||||
|
|
||||
|
return preg_replace_callback( |
||||
|
'/{\s*([A-Za-z_\-\.0-9]+)\s*}/', |
||||
|
function (array $matches) use ($request, $response, $error, &$cache) { |
||||
|
if (isset($cache[$matches[1]])) { |
||||
|
return $cache[$matches[1]]; |
||||
|
} |
||||
|
|
||||
|
$result = ''; |
||||
|
switch ($matches[1]) { |
||||
|
case 'request': |
||||
|
$result = Psr7\str($request); |
||||
|
break; |
||||
|
case 'response': |
||||
|
$result = $response ? Psr7\str($response) : ''; |
||||
|
break; |
||||
|
case 'req_headers': |
||||
|
$result = trim($request->getMethod() |
||||
|
. ' ' . $request->getRequestTarget()) |
||||
|
. ' HTTP/' . $request->getProtocolVersion() . "\r\n" |
||||
|
. $this->headers($request); |
||||
|
break; |
||||
|
case 'res_headers': |
||||
|
$result = $response ? |
||||
|
sprintf( |
||||
|
'HTTP/%s %d %s', |
||||
|
$response->getProtocolVersion(), |
||||
|
$response->getStatusCode(), |
||||
|
$response->getReasonPhrase() |
||||
|
) . "\r\n" . $this->headers($response) |
||||
|
: 'NULL'; |
||||
|
break; |
||||
|
case 'req_body': |
||||
|
$result = $request->getBody(); |
||||
|
break; |
||||
|
case 'res_body': |
||||
|
$result = $response ? $response->getBody() : 'NULL'; |
||||
|
break; |
||||
|
case 'ts': |
||||
|
case 'date_iso_8601': |
||||
|
$result = gmdate('c'); |
||||
|
break; |
||||
|
case 'date_common_log': |
||||
|
$result = date('d/M/Y:H:i:s O'); |
||||
|
break; |
||||
|
case 'method': |
||||
|
$result = $request->getMethod(); |
||||
|
break; |
||||
|
case 'version': |
||||
|
$result = $request->getProtocolVersion(); |
||||
|
break; |
||||
|
case 'uri': |
||||
|
case 'url': |
||||
|
$result = $request->getUri(); |
||||
|
break; |
||||
|
case 'target': |
||||
|
$result = $request->getRequestTarget(); |
||||
|
break; |
||||
|
case 'req_version': |
||||
|
$result = $request->getProtocolVersion(); |
||||
|
break; |
||||
|
case 'res_version': |
||||
|
$result = $response |
||||
|
? $response->getProtocolVersion() |
||||
|
: 'NULL'; |
||||
|
break; |
||||
|
case 'host': |
||||
|
$result = $request->getHeaderLine('Host'); |
||||
|
break; |
||||
|
case 'hostname': |
||||
|
$result = gethostname(); |
||||
|
break; |
||||
|
case 'code': |
||||
|
$result = $response ? $response->getStatusCode() : 'NULL'; |
||||
|
break; |
||||
|
case 'phrase': |
||||
|
$result = $response ? $response->getReasonPhrase() : 'NULL'; |
||||
|
break; |
||||
|
case 'error': |
||||
|
$result = $error ? $error->getMessage() : 'NULL'; |
||||
|
break; |
||||
|
default: |
||||
|
// handle prefixed dynamic headers |
||||
|
if (strpos($matches[1], 'req_header_') === 0) { |
||||
|
$result = $request->getHeaderLine(substr($matches[1], 11)); |
||||
|
} elseif (strpos($matches[1], 'res_header_') === 0) { |
||||
|
$result = $response |
||||
|
? $response->getHeaderLine(substr($matches[1], 11)) |
||||
|
: 'NULL'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$cache[$matches[1]] = $result; |
||||
|
return $result; |
||||
|
}, |
||||
|
$this->template |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private function headers(MessageInterface $message) |
||||
|
{ |
||||
|
$result = ''; |
||||
|
foreach ($message->getHeaders() as $name => $values) { |
||||
|
$result .= $name . ': ' . implode(', ', $values) . "\r\n"; |
||||
|
} |
||||
|
|
||||
|
return trim($result); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,255 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp; |
||||
|
|
||||
|
use GuzzleHttp\Cookie\CookieJarInterface; |
||||
|
use GuzzleHttp\Exception\RequestException; |
||||
|
use GuzzleHttp\Promise\RejectedPromise; |
||||
|
use GuzzleHttp\Psr7; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
use Psr\Log\LoggerInterface; |
||||
|
use Psr\Log\LogLevel; |
||||
|
|
||||
|
/** |
||||
|
* Functions used to create and wrap handlers with handler middleware. |
||||
|
*/ |
||||
|
final class Middleware |
||||
|
{ |
||||
|
/** |
||||
|
* Middleware that adds cookies to requests. |
||||
|
* |
||||
|
* The options array must be set to a CookieJarInterface in order to use |
||||
|
* cookies. This is typically handled for you by a client. |
||||
|
* |
||||
|
* @return callable Returns a function that accepts the next handler. |
||||
|
*/ |
||||
|
public static function cookies() |
||||
|
{ |
||||
|
return function (callable $handler) { |
||||
|
return function ($request, array $options) use ($handler) { |
||||
|
if (empty($options['cookies'])) { |
||||
|
return $handler($request, $options); |
||||
|
} elseif (!($options['cookies'] instanceof CookieJarInterface)) { |
||||
|
throw new \InvalidArgumentException('cookies must be an instance of GuzzleHttp\Cookie\CookieJarInterface'); |
||||
|
} |
||||
|
$cookieJar = $options['cookies']; |
||||
|
$request = $cookieJar->withCookieHeader($request); |
||||
|
return $handler($request, $options) |
||||
|
->then( |
||||
|
function ($response) use ($cookieJar, $request) { |
||||
|
$cookieJar->extractCookies($request, $response); |
||||
|
return $response; |
||||
|
} |
||||
|
); |
||||
|
}; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Middleware that throws exceptions for 4xx or 5xx responses when the |
||||
|
* "http_error" request option is set to true. |
||||
|
* |
||||
|
* @return callable Returns a function that accepts the next handler. |
||||
|
*/ |
||||
|
public static function httpErrors() |
||||
|
{ |
||||
|
return function (callable $handler) { |
||||
|
return function ($request, array $options) use ($handler) { |
||||
|
if (empty($options['http_errors'])) { |
||||
|
return $handler($request, $options); |
||||
|
} |
||||
|
return $handler($request, $options)->then( |
||||
|
function (ResponseInterface $response) use ($request, $handler) { |
||||
|
$code = $response->getStatusCode(); |
||||
|
if ($code < 400) { |
||||
|
return $response; |
||||
|
} |
||||
|
throw RequestException::create($request, $response); |
||||
|
} |
||||
|
); |
||||
|
}; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Middleware that pushes history data to an ArrayAccess container. |
||||
|
* |
||||
|
* @param array|\ArrayAccess $container Container to hold the history (by reference). |
||||
|
* |
||||
|
* @return callable Returns a function that accepts the next handler. |
||||
|
* @throws \InvalidArgumentException if container is not an array or ArrayAccess. |
||||
|
*/ |
||||
|
public static function history(&$container) |
||||
|
{ |
||||
|
if (!is_array($container) && !$container instanceof \ArrayAccess) { |
||||
|
throw new \InvalidArgumentException('history container must be an array or object implementing ArrayAccess'); |
||||
|
} |
||||
|
|
||||
|
return function (callable $handler) use (&$container) { |
||||
|
return function ($request, array $options) use ($handler, &$container) { |
||||
|
return $handler($request, $options)->then( |
||||
|
function ($value) use ($request, &$container, $options) { |
||||
|
$container[] = [ |
||||
|
'request' => $request, |
||||
|
'response' => $value, |
||||
|
'error' => null, |
||||
|
'options' => $options |
||||
|
]; |
||||
|
return $value; |
||||
|
}, |
||||
|
function ($reason) use ($request, &$container, $options) { |
||||
|
$container[] = [ |
||||
|
'request' => $request, |
||||
|
'response' => null, |
||||
|
'error' => $reason, |
||||
|
'options' => $options |
||||
|
]; |
||||
|
return \GuzzleHttp\Promise\rejection_for($reason); |
||||
|
} |
||||
|
); |
||||
|
}; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Middleware that invokes a callback before and after sending a request. |
||||
|
* |
||||
|
* The provided listener cannot modify or alter the response. It simply |
||||
|
* "taps" into the chain to be notified before returning the promise. The |
||||
|
* before listener accepts a request and options array, and the after |
||||
|
* listener accepts a request, options array, and response promise. |
||||
|
* |
||||
|
* @param callable $before Function to invoke before forwarding the request. |
||||
|
* @param callable $after Function invoked after forwarding. |
||||
|
* |
||||
|
* @return callable Returns a function that accepts the next handler. |
||||
|
*/ |
||||
|
public static function tap(callable $before = null, callable $after = null) |
||||
|
{ |
||||
|
return function (callable $handler) use ($before, $after) { |
||||
|
return function ($request, array $options) use ($handler, $before, $after) { |
||||
|
if ($before) { |
||||
|
$before($request, $options); |
||||
|
} |
||||
|
$response = $handler($request, $options); |
||||
|
if ($after) { |
||||
|
$after($request, $options, $response); |
||||
|
} |
||||
|
return $response; |
||||
|
}; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Middleware that handles request redirects. |
||||
|
* |
||||
|
* @return callable Returns a function that accepts the next handler. |
||||
|
*/ |
||||
|
public static function redirect() |
||||
|
{ |
||||
|
return function (callable $handler) { |
||||
|
return new RedirectMiddleware($handler); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Middleware that retries requests based on the boolean result of |
||||
|
* invoking the provided "decider" function. |
||||
|
* |
||||
|
* If no delay function is provided, a simple implementation of exponential |
||||
|
* backoff will be utilized. |
||||
|
* |
||||
|
* @param callable $decider Function that accepts the number of retries, |
||||
|
* a request, [response], and [exception] and |
||||
|
* returns true if the request is to be retried. |
||||
|
* @param callable $delay Function that accepts the number of retries and |
||||
|
* returns the number of milliseconds to delay. |
||||
|
* |
||||
|
* @return callable Returns a function that accepts the next handler. |
||||
|
*/ |
||||
|
public static function retry(callable $decider, callable $delay = null) |
||||
|
{ |
||||
|
return function (callable $handler) use ($decider, $delay) { |
||||
|
return new RetryMiddleware($decider, $handler, $delay); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Middleware that logs requests, responses, and errors using a message |
||||
|
* formatter. |
||||
|
* |
||||
|
* @param LoggerInterface $logger Logs messages. |
||||
|
* @param MessageFormatter $formatter Formatter used to create message strings. |
||||
|
* @param string $logLevel Level at which to log requests. |
||||
|
* |
||||
|
* @return callable Returns a function that accepts the next handler. |
||||
|
*/ |
||||
|
public static function log(LoggerInterface $logger, MessageFormatter $formatter, $logLevel = LogLevel::INFO) |
||||
|
{ |
||||
|
return function (callable $handler) use ($logger, $formatter, $logLevel) { |
||||
|
return function ($request, array $options) use ($handler, $logger, $formatter, $logLevel) { |
||||
|
return $handler($request, $options)->then( |
||||
|
function ($response) use ($logger, $request, $formatter, $logLevel) { |
||||
|
$message = $formatter->format($request, $response); |
||||
|
$logger->log($logLevel, $message); |
||||
|
return $response; |
||||
|
}, |
||||
|
function ($reason) use ($logger, $request, $formatter) { |
||||
|
$response = $reason instanceof RequestException |
||||
|
? $reason->getResponse() |
||||
|
: null; |
||||
|
$message = $formatter->format($request, $response, $reason); |
||||
|
$logger->notice($message); |
||||
|
return \GuzzleHttp\Promise\rejection_for($reason); |
||||
|
} |
||||
|
); |
||||
|
}; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* This middleware adds a default content-type if possible, a default |
||||
|
* content-length or transfer-encoding header, and the expect header. |
||||
|
* |
||||
|
* @return callable |
||||
|
*/ |
||||
|
public static function prepareBody() |
||||
|
{ |
||||
|
return function (callable $handler) { |
||||
|
return new PrepareBodyMiddleware($handler); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Middleware that applies a map function to the request before passing to |
||||
|
* the next handler. |
||||
|
* |
||||
|
* @param callable $fn Function that accepts a RequestInterface and returns |
||||
|
* a RequestInterface. |
||||
|
* @return callable |
||||
|
*/ |
||||
|
public static function mapRequest(callable $fn) |
||||
|
{ |
||||
|
return function (callable $handler) use ($fn) { |
||||
|
return function ($request, array $options) use ($handler, $fn) { |
||||
|
return $handler($fn($request), $options); |
||||
|
}; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Middleware that applies a map function to the resolved promise's |
||||
|
* response. |
||||
|
* |
||||
|
* @param callable $fn Function that accepts a ResponseInterface and |
||||
|
* returns a ResponseInterface. |
||||
|
* @return callable |
||||
|
*/ |
||||
|
public static function mapResponse(callable $fn) |
||||
|
{ |
||||
|
return function (callable $handler) use ($fn) { |
||||
|
return function ($request, array $options) use ($handler, $fn) { |
||||
|
return $handler($request, $options)->then($fn); |
||||
|
}; |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,123 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp; |
||||
|
|
||||
|
use GuzzleHttp\Promise\PromisorInterface; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use GuzzleHttp\Promise\EachPromise; |
||||
|
|
||||
|
/** |
||||
|
* Sends and iterator of requests concurrently using a capped pool size. |
||||
|
* |
||||
|
* The pool will read from an iterator until it is cancelled or until the |
||||
|
* iterator is consumed. When a request is yielded, the request is sent after |
||||
|
* applying the "request_options" request options (if provided in the ctor). |
||||
|
* |
||||
|
* When a function is yielded by the iterator, the function is provided the |
||||
|
* "request_options" array that should be merged on top of any existing |
||||
|
* options, and the function MUST then return a wait-able promise. |
||||
|
*/ |
||||
|
class Pool implements PromisorInterface |
||||
|
{ |
||||
|
/** @var EachPromise */ |
||||
|
private $each; |
||||
|
|
||||
|
/** |
||||
|
* @param ClientInterface $client Client used to send the requests. |
||||
|
* @param array|\Iterator $requests Requests or functions that return |
||||
|
* requests to send concurrently. |
||||
|
* @param array $config Associative array of options |
||||
|
* - concurrency: (int) Maximum number of requests to send concurrently |
||||
|
* - options: Array of request options to apply to each request. |
||||
|
* - fulfilled: (callable) Function to invoke when a request completes. |
||||
|
* - rejected: (callable) Function to invoke when a request is rejected. |
||||
|
*/ |
||||
|
public function __construct( |
||||
|
ClientInterface $client, |
||||
|
$requests, |
||||
|
array $config = [] |
||||
|
) { |
||||
|
// Backwards compatibility. |
||||
|
if (isset($config['pool_size'])) { |
||||
|
$config['concurrency'] = $config['pool_size']; |
||||
|
} elseif (!isset($config['concurrency'])) { |
||||
|
$config['concurrency'] = 25; |
||||
|
} |
||||
|
|
||||
|
if (isset($config['options'])) { |
||||
|
$opts = $config['options']; |
||||
|
unset($config['options']); |
||||
|
} else { |
||||
|
$opts = []; |
||||
|
} |
||||
|
|
||||
|
$iterable = \GuzzleHttp\Promise\iter_for($requests); |
||||
|
$requests = function () use ($iterable, $client, $opts) { |
||||
|
foreach ($iterable as $key => $rfn) { |
||||
|
if ($rfn instanceof RequestInterface) { |
||||
|
yield $key => $client->sendAsync($rfn, $opts); |
||||
|
} elseif (is_callable($rfn)) { |
||||
|
yield $key => $rfn($opts); |
||||
|
} else { |
||||
|
throw new \InvalidArgumentException('Each value yielded by ' |
||||
|
. 'the iterator must be a Psr7\Http\Message\RequestInterface ' |
||||
|
. 'or a callable that returns a promise that fulfills ' |
||||
|
. 'with a Psr7\Message\Http\ResponseInterface object.'); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
$this->each = new EachPromise($requests(), $config); |
||||
|
} |
||||
|
|
||||
|
public function promise() |
||||
|
{ |
||||
|
return $this->each->promise(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Sends multiple requests concurrently and returns an array of responses |
||||
|
* and exceptions that uses the same ordering as the provided requests. |
||||
|
* |
||||
|
* IMPORTANT: This method keeps every request and response in memory, and |
||||
|
* as such, is NOT recommended when sending a large number or an |
||||
|
* indeterminate number of requests concurrently. |
||||
|
* |
||||
|
* @param ClientInterface $client Client used to send the requests |
||||
|
* @param array|\Iterator $requests Requests to send concurrently. |
||||
|
* @param array $options Passes through the options available in |
||||
|
* {@see GuzzleHttp\Pool::__construct} |
||||
|
* |
||||
|
* @return array Returns an array containing the response or an exception |
||||
|
* in the same order that the requests were sent. |
||||
|
* @throws \InvalidArgumentException if the event format is incorrect. |
||||
|
*/ |
||||
|
public static function batch( |
||||
|
ClientInterface $client, |
||||
|
$requests, |
||||
|
array $options = [] |
||||
|
) { |
||||
|
$res = []; |
||||
|
self::cmpCallback($options, 'fulfilled', $res); |
||||
|
self::cmpCallback($options, 'rejected', $res); |
||||
|
$pool = new static($client, $requests, $options); |
||||
|
$pool->promise()->wait(); |
||||
|
ksort($res); |
||||
|
|
||||
|
return $res; |
||||
|
} |
||||
|
|
||||
|
private static function cmpCallback(array &$options, $name, array &$results) |
||||
|
{ |
||||
|
if (!isset($options[$name])) { |
||||
|
$options[$name] = function ($v, $k) use (&$results) { |
||||
|
$results[$k] = $v; |
||||
|
}; |
||||
|
} else { |
||||
|
$currentFn = $options[$name]; |
||||
|
$options[$name] = function ($v, $k) use (&$results, $currentFn) { |
||||
|
$currentFn($v, $k); |
||||
|
$results[$k] = $v; |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp; |
||||
|
|
||||
|
use GuzzleHttp\Promise\PromiseInterface; |
||||
|
use GuzzleHttp\Psr7; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
|
||||
|
/** |
||||
|
* Prepares requests that contain a body, adding the Content-Length, |
||||
|
* Content-Type, and Expect headers. |
||||
|
*/ |
||||
|
class PrepareBodyMiddleware |
||||
|
{ |
||||
|
/** @var callable */ |
||||
|
private $nextHandler; |
||||
|
|
||||
|
/** |
||||
|
* @param callable $nextHandler Next handler to invoke. |
||||
|
*/ |
||||
|
public function __construct(callable $nextHandler) |
||||
|
{ |
||||
|
$this->nextHandler = $nextHandler; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param RequestInterface $request |
||||
|
* @param array $options |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
public function __invoke(RequestInterface $request, array $options) |
||||
|
{ |
||||
|
$fn = $this->nextHandler; |
||||
|
|
||||
|
// Don't do anything if the request has no body. |
||||
|
if ($request->getBody()->getSize() === 0) { |
||||
|
return $fn($request, $options); |
||||
|
} |
||||
|
|
||||
|
$modify = []; |
||||
|
|
||||
|
// Add a default content-type if possible. |
||||
|
if (!$request->hasHeader('Content-Type')) { |
||||
|
if ($uri = $request->getBody()->getMetadata('uri')) { |
||||
|
if ($type = Psr7\mimetype_from_filename($uri)) { |
||||
|
$modify['set_headers']['Content-Type'] = $type; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Add a default content-length or transfer-encoding header. |
||||
|
if (!$request->hasHeader('Content-Length') |
||||
|
&& !$request->hasHeader('Transfer-Encoding') |
||||
|
) { |
||||
|
$size = $request->getBody()->getSize(); |
||||
|
if ($size !== null) { |
||||
|
$modify['set_headers']['Content-Length'] = $size; |
||||
|
} else { |
||||
|
$modify['set_headers']['Transfer-Encoding'] = 'chunked'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Add the expect header if needed. |
||||
|
$this->addExpectHeader($request, $options, $modify); |
||||
|
|
||||
|
return $fn(Psr7\modify_request($request, $modify), $options); |
||||
|
} |
||||
|
|
||||
|
private function addExpectHeader( |
||||
|
RequestInterface $request, |
||||
|
array $options, |
||||
|
array &$modify |
||||
|
) { |
||||
|
// Determine if the Expect header should be used |
||||
|
if ($request->hasHeader('Expect')) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
$expect = isset($options['expect']) ? $options['expect'] : null; |
||||
|
|
||||
|
// Return if disabled or if you're not using HTTP/1.1 or HTTP/2.0 |
||||
|
if ($expect === false || $request->getProtocolVersion() < 1.1) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// The expect header is unconditionally enabled |
||||
|
if ($expect === true) { |
||||
|
$modify['set_headers']['Expect'] = '100-Continue'; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// By default, send the expect header when the payload is > 1mb |
||||
|
if ($expect === null) { |
||||
|
$expect = 1048576; |
||||
|
} |
||||
|
|
||||
|
// Always add if the body cannot be rewound, the size cannot be |
||||
|
// determined, or the size is greater than the cutoff threshold |
||||
|
$body = $request->getBody(); |
||||
|
$size = $body->getSize(); |
||||
|
|
||||
|
if ($size === null || $size >= (int) $expect || !$body->isSeekable()) { |
||||
|
$modify['set_headers']['Expect'] = '100-Continue'; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,237 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp; |
||||
|
|
||||
|
use GuzzleHttp\Exception\BadResponseException; |
||||
|
use GuzzleHttp\Exception\TooManyRedirectsException; |
||||
|
use GuzzleHttp\Promise\PromiseInterface; |
||||
|
use GuzzleHttp\Psr7; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
use Psr\Http\Message\UriInterface; |
||||
|
|
||||
|
/** |
||||
|
* Request redirect middleware. |
||||
|
* |
||||
|
* Apply this middleware like other middleware using |
||||
|
* {@see GuzzleHttp\Middleware::redirect()}. |
||||
|
*/ |
||||
|
class RedirectMiddleware |
||||
|
{ |
||||
|
const HISTORY_HEADER = 'X-Guzzle-Redirect-History'; |
||||
|
|
||||
|
const STATUS_HISTORY_HEADER = 'X-Guzzle-Redirect-Status-History'; |
||||
|
|
||||
|
public static $defaultSettings = [ |
||||
|
'max' => 5, |
||||
|
'protocols' => ['http', 'https'], |
||||
|
'strict' => false, |
||||
|
'referer' => false, |
||||
|
'track_redirects' => false, |
||||
|
]; |
||||
|
|
||||
|
/** @var callable */ |
||||
|
private $nextHandler; |
||||
|
|
||||
|
/** |
||||
|
* @param callable $nextHandler Next handler to invoke. |
||||
|
*/ |
||||
|
public function __construct(callable $nextHandler) |
||||
|
{ |
||||
|
$this->nextHandler = $nextHandler; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param RequestInterface $request |
||||
|
* @param array $options |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
public function __invoke(RequestInterface $request, array $options) |
||||
|
{ |
||||
|
$fn = $this->nextHandler; |
||||
|
|
||||
|
if (empty($options['allow_redirects'])) { |
||||
|
return $fn($request, $options); |
||||
|
} |
||||
|
|
||||
|
if ($options['allow_redirects'] === true) { |
||||
|
$options['allow_redirects'] = self::$defaultSettings; |
||||
|
} elseif (!is_array($options['allow_redirects'])) { |
||||
|
throw new \InvalidArgumentException('allow_redirects must be true, false, or array'); |
||||
|
} else { |
||||
|
// Merge the default settings with the provided settings |
||||
|
$options['allow_redirects'] += self::$defaultSettings; |
||||
|
} |
||||
|
|
||||
|
if (empty($options['allow_redirects']['max'])) { |
||||
|
return $fn($request, $options); |
||||
|
} |
||||
|
|
||||
|
return $fn($request, $options) |
||||
|
->then(function (ResponseInterface $response) use ($request, $options) { |
||||
|
return $this->checkRedirect($request, $options, $response); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param RequestInterface $request |
||||
|
* @param array $options |
||||
|
* @param ResponseInterface|PromiseInterface $response |
||||
|
* |
||||
|
* @return ResponseInterface|PromiseInterface |
||||
|
*/ |
||||
|
public function checkRedirect( |
||||
|
RequestInterface $request, |
||||
|
array $options, |
||||
|
ResponseInterface $response |
||||
|
) { |
||||
|
if (substr($response->getStatusCode(), 0, 1) != '3' |
||||
|
|| !$response->hasHeader('Location') |
||||
|
) { |
||||
|
return $response; |
||||
|
} |
||||
|
|
||||
|
$this->guardMax($request, $options); |
||||
|
$nextRequest = $this->modifyRequest($request, $options, $response); |
||||
|
|
||||
|
if (isset($options['allow_redirects']['on_redirect'])) { |
||||
|
call_user_func( |
||||
|
$options['allow_redirects']['on_redirect'], |
||||
|
$request, |
||||
|
$response, |
||||
|
$nextRequest->getUri() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** @var PromiseInterface|ResponseInterface $promise */ |
||||
|
$promise = $this($nextRequest, $options); |
||||
|
|
||||
|
// Add headers to be able to track history of redirects. |
||||
|
if (!empty($options['allow_redirects']['track_redirects'])) { |
||||
|
return $this->withTracking( |
||||
|
$promise, |
||||
|
(string) $nextRequest->getUri(), |
||||
|
$response->getStatusCode() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return $promise; |
||||
|
} |
||||
|
|
||||
|
private function withTracking(PromiseInterface $promise, $uri, $statusCode) |
||||
|
{ |
||||
|
return $promise->then( |
||||
|
function (ResponseInterface $response) use ($uri, $statusCode) { |
||||
|
// Note that we are pushing to the front of the list as this |
||||
|
// would be an earlier response than what is currently present |
||||
|
// in the history header. |
||||
|
$historyHeader = $response->getHeader(self::HISTORY_HEADER); |
||||
|
$statusHeader = $response->getHeader(self::STATUS_HISTORY_HEADER); |
||||
|
array_unshift($historyHeader, $uri); |
||||
|
array_unshift($statusHeader, $statusCode); |
||||
|
return $response->withHeader(self::HISTORY_HEADER, $historyHeader) |
||||
|
->withHeader(self::STATUS_HISTORY_HEADER, $statusHeader); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private function guardMax(RequestInterface $request, array &$options) |
||||
|
{ |
||||
|
$current = isset($options['__redirect_count']) |
||||
|
? $options['__redirect_count'] |
||||
|
: 0; |
||||
|
$options['__redirect_count'] = $current + 1; |
||||
|
$max = $options['allow_redirects']['max']; |
||||
|
|
||||
|
if ($options['__redirect_count'] > $max) { |
||||
|
throw new TooManyRedirectsException( |
||||
|
"Will not follow more than {$max} redirects", |
||||
|
$request |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param RequestInterface $request |
||||
|
* @param array $options |
||||
|
* @param ResponseInterface $response |
||||
|
* |
||||
|
* @return RequestInterface |
||||
|
*/ |
||||
|
public function modifyRequest( |
||||
|
RequestInterface $request, |
||||
|
array $options, |
||||
|
ResponseInterface $response |
||||
|
) { |
||||
|
// Request modifications to apply. |
||||
|
$modify = []; |
||||
|
$protocols = $options['allow_redirects']['protocols']; |
||||
|
|
||||
|
// Use a GET request if this is an entity enclosing request and we are |
||||
|
// not forcing RFC compliance, but rather emulating what all browsers |
||||
|
// would do. |
||||
|
$statusCode = $response->getStatusCode(); |
||||
|
if ($statusCode == 303 || |
||||
|
($statusCode <= 302 && $request->getBody() && !$options['allow_redirects']['strict']) |
||||
|
) { |
||||
|
$modify['method'] = 'GET'; |
||||
|
$modify['body'] = ''; |
||||
|
} |
||||
|
|
||||
|
$modify['uri'] = $this->redirectUri($request, $response, $protocols); |
||||
|
Psr7\rewind_body($request); |
||||
|
|
||||
|
// Add the Referer header if it is told to do so and only |
||||
|
// add the header if we are not redirecting from https to http. |
||||
|
if ($options['allow_redirects']['referer'] |
||||
|
&& $modify['uri']->getScheme() === $request->getUri()->getScheme() |
||||
|
) { |
||||
|
$uri = $request->getUri()->withUserInfo('', ''); |
||||
|
$modify['set_headers']['Referer'] = (string) $uri; |
||||
|
} else { |
||||
|
$modify['remove_headers'][] = 'Referer'; |
||||
|
} |
||||
|
|
||||
|
// Remove Authorization header if host is different. |
||||
|
if ($request->getUri()->getHost() !== $modify['uri']->getHost()) { |
||||
|
$modify['remove_headers'][] = 'Authorization'; |
||||
|
} |
||||
|
|
||||
|
return Psr7\modify_request($request, $modify); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set the appropriate URL on the request based on the location header |
||||
|
* |
||||
|
* @param RequestInterface $request |
||||
|
* @param ResponseInterface $response |
||||
|
* @param array $protocols |
||||
|
* |
||||
|
* @return UriInterface |
||||
|
*/ |
||||
|
private function redirectUri( |
||||
|
RequestInterface $request, |
||||
|
ResponseInterface $response, |
||||
|
array $protocols |
||||
|
) { |
||||
|
$location = Psr7\UriResolver::resolve( |
||||
|
$request->getUri(), |
||||
|
new Psr7\Uri($response->getHeaderLine('Location')) |
||||
|
); |
||||
|
|
||||
|
// Ensure that the redirect URI is allowed based on the protocols. |
||||
|
if (!in_array($location->getScheme(), $protocols)) { |
||||
|
throw new BadResponseException( |
||||
|
sprintf( |
||||
|
'Redirect URI, %s, does not use one of the allowed redirect protocols: %s', |
||||
|
$location, |
||||
|
implode(', ', $protocols) |
||||
|
), |
||||
|
$request, |
||||
|
$response |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return $location; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,255 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp; |
||||
|
|
||||
|
/** |
||||
|
* This class contains a list of built-in Guzzle request options. |
||||
|
* |
||||
|
* More documentation for each option can be found at http://guzzlephp.org/. |
||||
|
* |
||||
|
* @link http://docs.guzzlephp.org/en/v6/request-options.html |
||||
|
*/ |
||||
|
final class RequestOptions |
||||
|
{ |
||||
|
/** |
||||
|
* allow_redirects: (bool|array) Controls redirect behavior. Pass false |
||||
|
* to disable redirects, pass true to enable redirects, pass an |
||||
|
* associative to provide custom redirect settings. Defaults to "false". |
||||
|
* This option only works if your handler has the RedirectMiddleware. When |
||||
|
* passing an associative array, you can provide the following key value |
||||
|
* pairs: |
||||
|
* |
||||
|
* - max: (int, default=5) maximum number of allowed redirects. |
||||
|
* - strict: (bool, default=false) Set to true to use strict redirects |
||||
|
* meaning redirect POST requests with POST requests vs. doing what most |
||||
|
* browsers do which is redirect POST requests with GET requests |
||||
|
* - referer: (bool, default=true) Set to false to disable the Referer |
||||
|
* header. |
||||
|
* - protocols: (array, default=['http', 'https']) Allowed redirect |
||||
|
* protocols. |
||||
|
* - on_redirect: (callable) PHP callable that is invoked when a redirect |
||||
|
* is encountered. The callable is invoked with the request, the redirect |
||||
|
* response that was received, and the effective URI. Any return value |
||||
|
* from the on_redirect function is ignored. |
||||
|
*/ |
||||
|
const ALLOW_REDIRECTS = 'allow_redirects'; |
||||
|
|
||||
|
/** |
||||
|
* auth: (array) Pass an array of HTTP authentication parameters to use |
||||
|
* with the request. The array must contain the username in index [0], |
||||
|
* the password in index [1], and you can optionally provide a built-in |
||||
|
* authentication type in index [2]. Pass null to disable authentication |
||||
|
* for a request. |
||||
|
*/ |
||||
|
const AUTH = 'auth'; |
||||
|
|
||||
|
/** |
||||
|
* body: (resource|string|null|int|float|StreamInterface|callable|\Iterator) |
||||
|
* Body to send in the request. |
||||
|
*/ |
||||
|
const BODY = 'body'; |
||||
|
|
||||
|
/** |
||||
|
* cert: (string|array) Set to a string to specify the path to a file |
||||
|
* containing a PEM formatted SSL client side certificate. If a password |
||||
|
* is required, then set cert to an array containing the path to the PEM |
||||
|
* file in the first array element followed by the certificate password |
||||
|
* in the second array element. |
||||
|
*/ |
||||
|
const CERT = 'cert'; |
||||
|
|
||||
|
/** |
||||
|
* cookies: (bool|GuzzleHttp\Cookie\CookieJarInterface, default=false) |
||||
|
* Specifies whether or not cookies are used in a request or what cookie |
||||
|
* jar to use or what cookies to send. This option only works if your |
||||
|
* handler has the `cookie` middleware. Valid values are `false` and |
||||
|
* an instance of {@see GuzzleHttp\Cookie\CookieJarInterface}. |
||||
|
*/ |
||||
|
const COOKIES = 'cookies'; |
||||
|
|
||||
|
/** |
||||
|
* connect_timeout: (float, default=0) Float describing the number of |
||||
|
* seconds to wait while trying to connect to a server. Use 0 to wait |
||||
|
* indefinitely (the default behavior). |
||||
|
*/ |
||||
|
const CONNECT_TIMEOUT = 'connect_timeout'; |
||||
|
|
||||
|
/** |
||||
|
* debug: (bool|resource) Set to true or set to a PHP stream returned by |
||||
|
* fopen() enable debug output with the HTTP handler used to send a |
||||
|
* request. |
||||
|
*/ |
||||
|
const DEBUG = 'debug'; |
||||
|
|
||||
|
/** |
||||
|
* decode_content: (bool, default=true) Specify whether or not |
||||
|
* Content-Encoding responses (gzip, deflate, etc.) are automatically |
||||
|
* decoded. |
||||
|
*/ |
||||
|
const DECODE_CONTENT = 'decode_content'; |
||||
|
|
||||
|
/** |
||||
|
* delay: (int) The amount of time to delay before sending in milliseconds. |
||||
|
*/ |
||||
|
const DELAY = 'delay'; |
||||
|
|
||||
|
/** |
||||
|
* expect: (bool|integer) Controls the behavior of the |
||||
|
* "Expect: 100-Continue" header. |
||||
|
* |
||||
|
* Set to `true` to enable the "Expect: 100-Continue" header for all |
||||
|
* requests that sends a body. Set to `false` to disable the |
||||
|
* "Expect: 100-Continue" header for all requests. Set to a number so that |
||||
|
* the size of the payload must be greater than the number in order to send |
||||
|
* the Expect header. Setting to a number will send the Expect header for |
||||
|
* all requests in which the size of the payload cannot be determined or |
||||
|
* where the body is not rewindable. |
||||
|
* |
||||
|
* By default, Guzzle will add the "Expect: 100-Continue" header when the |
||||
|
* size of the body of a request is greater than 1 MB and a request is |
||||
|
* using HTTP/1.1. |
||||
|
*/ |
||||
|
const EXPECT = 'expect'; |
||||
|
|
||||
|
/** |
||||
|
* form_params: (array) Associative array of form field names to values |
||||
|
* where each value is a string or array of strings. Sets the Content-Type |
||||
|
* header to application/x-www-form-urlencoded when no Content-Type header |
||||
|
* is already present. |
||||
|
*/ |
||||
|
const FORM_PARAMS = 'form_params'; |
||||
|
|
||||
|
/** |
||||
|
* headers: (array) Associative array of HTTP headers. Each value MUST be |
||||
|
* a string or array of strings. |
||||
|
*/ |
||||
|
const HEADERS = 'headers'; |
||||
|
|
||||
|
/** |
||||
|
* http_errors: (bool, default=true) Set to false to disable exceptions |
||||
|
* when a non- successful HTTP response is received. By default, |
||||
|
* exceptions will be thrown for 4xx and 5xx responses. This option only |
||||
|
* works if your handler has the `httpErrors` middleware. |
||||
|
*/ |
||||
|
const HTTP_ERRORS = 'http_errors'; |
||||
|
|
||||
|
/** |
||||
|
* json: (mixed) Adds JSON data to a request. The provided value is JSON |
||||
|
* encoded and a Content-Type header of application/json will be added to |
||||
|
* the request if no Content-Type header is already present. |
||||
|
*/ |
||||
|
const JSON = 'json'; |
||||
|
|
||||
|
/** |
||||
|
* multipart: (array) Array of associative arrays, each containing a |
||||
|
* required "name" key mapping to the form field, name, a required |
||||
|
* "contents" key mapping to a StreamInterface|resource|string, an |
||||
|
* optional "headers" associative array of custom headers, and an |
||||
|
* optional "filename" key mapping to a string to send as the filename in |
||||
|
* the part. If no "filename" key is present, then no "filename" attribute |
||||
|
* will be added to the part. |
||||
|
*/ |
||||
|
const MULTIPART = 'multipart'; |
||||
|
|
||||
|
/** |
||||
|
* on_headers: (callable) A callable that is invoked when the HTTP headers |
||||
|
* of the response have been received but the body has not yet begun to |
||||
|
* download. |
||||
|
*/ |
||||
|
const ON_HEADERS = 'on_headers'; |
||||
|
|
||||
|
/** |
||||
|
* on_stats: (callable) allows you to get access to transfer statistics of |
||||
|
* a request and access the lower level transfer details of the handler |
||||
|
* associated with your client. ``on_stats`` is a callable that is invoked |
||||
|
* when a handler has finished sending a request. The callback is invoked |
||||
|
* with transfer statistics about the request, the response received, or |
||||
|
* the error encountered. Included in the data is the total amount of time |
||||
|
* taken to send the request. |
||||
|
*/ |
||||
|
const ON_STATS = 'on_stats'; |
||||
|
|
||||
|
/** |
||||
|
* progress: (callable) Defines a function to invoke when transfer |
||||
|
* progress is made. The function accepts the following positional |
||||
|
* arguments: the total number of bytes expected to be downloaded, the |
||||
|
* number of bytes downloaded so far, the number of bytes expected to be |
||||
|
* uploaded, the number of bytes uploaded so far. |
||||
|
*/ |
||||
|
const PROGRESS = 'progress'; |
||||
|
|
||||
|
/** |
||||
|
* proxy: (string|array) Pass a string to specify an HTTP proxy, or an |
||||
|
* array to specify different proxies for different protocols (where the |
||||
|
* key is the protocol and the value is a proxy string). |
||||
|
*/ |
||||
|
const PROXY = 'proxy'; |
||||
|
|
||||
|
/** |
||||
|
* query: (array|string) Associative array of query string values to add |
||||
|
* to the request. This option uses PHP's http_build_query() to create |
||||
|
* the string representation. Pass a string value if you need more |
||||
|
* control than what this method provides |
||||
|
*/ |
||||
|
const QUERY = 'query'; |
||||
|
|
||||
|
/** |
||||
|
* sink: (resource|string|StreamInterface) Where the data of the |
||||
|
* response is written to. Defaults to a PHP temp stream. Providing a |
||||
|
* string will write data to a file by the given name. |
||||
|
*/ |
||||
|
const SINK = 'sink'; |
||||
|
|
||||
|
/** |
||||
|
* synchronous: (bool) Set to true to inform HTTP handlers that you intend |
||||
|
* on waiting on the response. This can be useful for optimizations. Note |
||||
|
* that a promise is still returned if you are using one of the async |
||||
|
* client methods. |
||||
|
*/ |
||||
|
const SYNCHRONOUS = 'synchronous'; |
||||
|
|
||||
|
/** |
||||
|
* ssl_key: (array|string) Specify the path to a file containing a private |
||||
|
* SSL key in PEM format. If a password is required, then set to an array |
||||
|
* containing the path to the SSL key in the first array element followed |
||||
|
* by the password required for the certificate in the second element. |
||||
|
*/ |
||||
|
const SSL_KEY = 'ssl_key'; |
||||
|
|
||||
|
/** |
||||
|
* stream: Set to true to attempt to stream a response rather than |
||||
|
* download it all up-front. |
||||
|
*/ |
||||
|
const STREAM = 'stream'; |
||||
|
|
||||
|
/** |
||||
|
* verify: (bool|string, default=true) Describes the SSL certificate |
||||
|
* verification behavior of a request. Set to true to enable SSL |
||||
|
* certificate verification using the system CA bundle when available |
||||
|
* (the default). Set to false to disable certificate verification (this |
||||
|
* is insecure!). Set to a string to provide the path to a CA bundle on |
||||
|
* disk to enable verification using a custom certificate. |
||||
|
*/ |
||||
|
const VERIFY = 'verify'; |
||||
|
|
||||
|
/** |
||||
|
* timeout: (float, default=0) Float describing the timeout of the |
||||
|
* request in seconds. Use 0 to wait indefinitely (the default behavior). |
||||
|
*/ |
||||
|
const TIMEOUT = 'timeout'; |
||||
|
|
||||
|
/** |
||||
|
* read_timeout: (float, default=default_socket_timeout ini setting) Float describing |
||||
|
* the body read timeout, for stream requests. |
||||
|
*/ |
||||
|
const READ_TIMEOUT = 'read_timeout'; |
||||
|
|
||||
|
/** |
||||
|
* version: (float) Specifies the HTTP protocol version to attempt to use. |
||||
|
*/ |
||||
|
const VERSION = 'version'; |
||||
|
|
||||
|
/** |
||||
|
* force_ip_resolve: (bool) Force client to use only ipv4 or ipv6 protocol |
||||
|
*/ |
||||
|
const FORCE_IP_RESOLVE = 'force_ip_resolve'; |
||||
|
} |
||||
@ -0,0 +1,112 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp; |
||||
|
|
||||
|
use GuzzleHttp\Promise\PromiseInterface; |
||||
|
use GuzzleHttp\Promise\RejectedPromise; |
||||
|
use GuzzleHttp\Psr7; |
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
|
||||
|
/** |
||||
|
* Middleware that retries requests based on the boolean result of |
||||
|
* invoking the provided "decider" function. |
||||
|
*/ |
||||
|
class RetryMiddleware |
||||
|
{ |
||||
|
/** @var callable */ |
||||
|
private $nextHandler; |
||||
|
|
||||
|
/** @var callable */ |
||||
|
private $decider; |
||||
|
|
||||
|
/** |
||||
|
* @param callable $decider Function that accepts the number of retries, |
||||
|
* a request, [response], and [exception] and |
||||
|
* returns true if the request is to be |
||||
|
* retried. |
||||
|
* @param callable $nextHandler Next handler to invoke. |
||||
|
* @param callable $delay Function that accepts the number of retries |
||||
|
* and [response] and returns the number of |
||||
|
* milliseconds to delay. |
||||
|
*/ |
||||
|
public function __construct( |
||||
|
callable $decider, |
||||
|
callable $nextHandler, |
||||
|
callable $delay = null |
||||
|
) { |
||||
|
$this->decider = $decider; |
||||
|
$this->nextHandler = $nextHandler; |
||||
|
$this->delay = $delay ?: __CLASS__ . '::exponentialDelay'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Default exponential backoff delay function. |
||||
|
* |
||||
|
* @param $retries |
||||
|
* |
||||
|
* @return int |
||||
|
*/ |
||||
|
public static function exponentialDelay($retries) |
||||
|
{ |
||||
|
return (int) pow(2, $retries - 1); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param RequestInterface $request |
||||
|
* @param array $options |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
public function __invoke(RequestInterface $request, array $options) |
||||
|
{ |
||||
|
if (!isset($options['retries'])) { |
||||
|
$options['retries'] = 0; |
||||
|
} |
||||
|
|
||||
|
$fn = $this->nextHandler; |
||||
|
return $fn($request, $options) |
||||
|
->then( |
||||
|
$this->onFulfilled($request, $options), |
||||
|
$this->onRejected($request, $options) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private function onFulfilled(RequestInterface $req, array $options) |
||||
|
{ |
||||
|
return function ($value) use ($req, $options) { |
||||
|
if (!call_user_func( |
||||
|
$this->decider, |
||||
|
$options['retries'], |
||||
|
$req, |
||||
|
$value, |
||||
|
null |
||||
|
)) { |
||||
|
return $value; |
||||
|
} |
||||
|
return $this->doRetry($req, $options, $value); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private function onRejected(RequestInterface $req, array $options) |
||||
|
{ |
||||
|
return function ($reason) use ($req, $options) { |
||||
|
if (!call_user_func( |
||||
|
$this->decider, |
||||
|
$options['retries'], |
||||
|
$req, |
||||
|
null, |
||||
|
$reason |
||||
|
)) { |
||||
|
return \GuzzleHttp\Promise\rejection_for($reason); |
||||
|
} |
||||
|
return $this->doRetry($req, $options); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private function doRetry(RequestInterface $request, array $options, ResponseInterface $response = null) |
||||
|
{ |
||||
|
$options['delay'] = call_user_func($this->delay, ++$options['retries'], $response); |
||||
|
|
||||
|
return $this($request, $options); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp; |
||||
|
|
||||
|
use Psr\Http\Message\RequestInterface; |
||||
|
use Psr\Http\Message\ResponseInterface; |
||||
|
use Psr\Http\Message\UriInterface; |
||||
|
|
||||
|
/** |
||||
|
* Represents data at the point after it was transferred either successfully |
||||
|
* or after a network error. |
||||
|
*/ |
||||
|
final class TransferStats |
||||
|
{ |
||||
|
private $request; |
||||
|
private $response; |
||||
|
private $transferTime; |
||||
|
private $handlerStats; |
||||
|
private $handlerErrorData; |
||||
|
|
||||
|
/** |
||||
|
* @param RequestInterface $request Request that was sent. |
||||
|
* @param ResponseInterface $response Response received (if any) |
||||
|
* @param null $transferTime Total handler transfer time. |
||||
|
* @param mixed $handlerErrorData Handler error data. |
||||
|
* @param array $handlerStats Handler specific stats. |
||||
|
*/ |
||||
|
public function __construct( |
||||
|
RequestInterface $request, |
||||
|
ResponseInterface $response = null, |
||||
|
$transferTime = null, |
||||
|
$handlerErrorData = null, |
||||
|
$handlerStats = [] |
||||
|
) { |
||||
|
$this->request = $request; |
||||
|
$this->response = $response; |
||||
|
$this->transferTime = $transferTime; |
||||
|
$this->handlerErrorData = $handlerErrorData; |
||||
|
$this->handlerStats = $handlerStats; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return RequestInterface |
||||
|
*/ |
||||
|
public function getRequest() |
||||
|
{ |
||||
|
return $this->request; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the response that was received (if any). |
||||
|
* |
||||
|
* @return ResponseInterface|null |
||||
|
*/ |
||||
|
public function getResponse() |
||||
|
{ |
||||
|
return $this->response; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns true if a response was received. |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function hasResponse() |
||||
|
{ |
||||
|
return $this->response !== null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Gets handler specific error data. |
||||
|
* |
||||
|
* This might be an exception, a integer representing an error code, or |
||||
|
* anything else. Relying on this value assumes that you know what handler |
||||
|
* you are using. |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function getHandlerErrorData() |
||||
|
{ |
||||
|
return $this->handlerErrorData; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the effective URI the request was sent to. |
||||
|
* |
||||
|
* @return UriInterface |
||||
|
*/ |
||||
|
public function getEffectiveUri() |
||||
|
{ |
||||
|
return $this->request->getUri(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the estimated time the request was being transferred by the handler. |
||||
|
* |
||||
|
* @return float Time in seconds. |
||||
|
*/ |
||||
|
public function getTransferTime() |
||||
|
{ |
||||
|
return $this->transferTime; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Gets an array of all of the handler specific transfer data. |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function getHandlerStats() |
||||
|
{ |
||||
|
return $this->handlerStats; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get a specific handler statistic from the handler by name. |
||||
|
* |
||||
|
* @param string $stat Handler specific transfer stat to retrieve. |
||||
|
* |
||||
|
* @return mixed|null |
||||
|
*/ |
||||
|
public function getHandlerStat($stat) |
||||
|
{ |
||||
|
return isset($this->handlerStats[$stat]) |
||||
|
? $this->handlerStats[$stat] |
||||
|
: null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,237 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp; |
||||
|
|
||||
|
/** |
||||
|
* Expands URI templates. Userland implementation of PECL uri_template. |
||||
|
* |
||||
|
* @link http://tools.ietf.org/html/rfc6570 |
||||
|
*/ |
||||
|
class UriTemplate |
||||
|
{ |
||||
|
/** @var string URI template */ |
||||
|
private $template; |
||||
|
|
||||
|
/** @var array Variables to use in the template expansion */ |
||||
|
private $variables; |
||||
|
|
||||
|
/** @var array Hash for quick operator lookups */ |
||||
|
private static $operatorHash = [ |
||||
|
'' => ['prefix' => '', 'joiner' => ',', 'query' => false], |
||||
|
'+' => ['prefix' => '', 'joiner' => ',', 'query' => false], |
||||
|
'#' => ['prefix' => '#', 'joiner' => ',', 'query' => false], |
||||
|
'.' => ['prefix' => '.', 'joiner' => '.', 'query' => false], |
||||
|
'/' => ['prefix' => '/', 'joiner' => '/', 'query' => false], |
||||
|
';' => ['prefix' => ';', 'joiner' => ';', 'query' => true], |
||||
|
'?' => ['prefix' => '?', 'joiner' => '&', 'query' => true], |
||||
|
'&' => ['prefix' => '&', 'joiner' => '&', 'query' => true] |
||||
|
]; |
||||
|
|
||||
|
/** @var array Delimiters */ |
||||
|
private static $delims = [':', '/', '?', '#', '[', ']', '@', '!', '$', |
||||
|
'&', '\'', '(', ')', '*', '+', ',', ';', '=']; |
||||
|
|
||||
|
/** @var array Percent encoded delimiters */ |
||||
|
private static $delimsPct = ['%3A', '%2F', '%3F', '%23', '%5B', '%5D', |
||||
|
'%40', '%21', '%24', '%26', '%27', '%28', '%29', '%2A', '%2B', '%2C', |
||||
|
'%3B', '%3D']; |
||||
|
|
||||
|
public function expand($template, array $variables) |
||||
|
{ |
||||
|
if (false === strpos($template, '{')) { |
||||
|
return $template; |
||||
|
} |
||||
|
|
||||
|
$this->template = $template; |
||||
|
$this->variables = $variables; |
||||
|
|
||||
|
return preg_replace_callback( |
||||
|
'/\{([^\}]+)\}/', |
||||
|
[$this, 'expandMatch'], |
||||
|
$this->template |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Parse an expression into parts |
||||
|
* |
||||
|
* @param string $expression Expression to parse |
||||
|
* |
||||
|
* @return array Returns an associative array of parts |
||||
|
*/ |
||||
|
private function parseExpression($expression) |
||||
|
{ |
||||
|
$result = []; |
||||
|
|
||||
|
if (isset(self::$operatorHash[$expression[0]])) { |
||||
|
$result['operator'] = $expression[0]; |
||||
|
$expression = substr($expression, 1); |
||||
|
} else { |
||||
|
$result['operator'] = ''; |
||||
|
} |
||||
|
|
||||
|
foreach (explode(',', $expression) as $value) { |
||||
|
$value = trim($value); |
||||
|
$varspec = []; |
||||
|
if ($colonPos = strpos($value, ':')) { |
||||
|
$varspec['value'] = substr($value, 0, $colonPos); |
||||
|
$varspec['modifier'] = ':'; |
||||
|
$varspec['position'] = (int) substr($value, $colonPos + 1); |
||||
|
} elseif (substr($value, -1) === '*') { |
||||
|
$varspec['modifier'] = '*'; |
||||
|
$varspec['value'] = substr($value, 0, -1); |
||||
|
} else { |
||||
|
$varspec['value'] = (string) $value; |
||||
|
$varspec['modifier'] = ''; |
||||
|
} |
||||
|
$result['values'][] = $varspec; |
||||
|
} |
||||
|
|
||||
|
return $result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Process an expansion |
||||
|
* |
||||
|
* @param array $matches Matches met in the preg_replace_callback |
||||
|
* |
||||
|
* @return string Returns the replacement string |
||||
|
*/ |
||||
|
private function expandMatch(array $matches) |
||||
|
{ |
||||
|
static $rfc1738to3986 = ['+' => '%20', '%7e' => '~']; |
||||
|
|
||||
|
$replacements = []; |
||||
|
$parsed = self::parseExpression($matches[1]); |
||||
|
$prefix = self::$operatorHash[$parsed['operator']]['prefix']; |
||||
|
$joiner = self::$operatorHash[$parsed['operator']]['joiner']; |
||||
|
$useQuery = self::$operatorHash[$parsed['operator']]['query']; |
||||
|
|
||||
|
foreach ($parsed['values'] as $value) { |
||||
|
if (!isset($this->variables[$value['value']])) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
$variable = $this->variables[$value['value']]; |
||||
|
$actuallyUseQuery = $useQuery; |
||||
|
$expanded = ''; |
||||
|
|
||||
|
if (is_array($variable)) { |
||||
|
$isAssoc = $this->isAssoc($variable); |
||||
|
$kvp = []; |
||||
|
foreach ($variable as $key => $var) { |
||||
|
if ($isAssoc) { |
||||
|
$key = rawurlencode($key); |
||||
|
$isNestedArray = is_array($var); |
||||
|
} else { |
||||
|
$isNestedArray = false; |
||||
|
} |
||||
|
|
||||
|
if (!$isNestedArray) { |
||||
|
$var = rawurlencode($var); |
||||
|
if ($parsed['operator'] === '+' || |
||||
|
$parsed['operator'] === '#' |
||||
|
) { |
||||
|
$var = $this->decodeReserved($var); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($value['modifier'] === '*') { |
||||
|
if ($isAssoc) { |
||||
|
if ($isNestedArray) { |
||||
|
// Nested arrays must allow for deeply nested |
||||
|
// structures. |
||||
|
$var = strtr( |
||||
|
http_build_query([$key => $var]), |
||||
|
$rfc1738to3986 |
||||
|
); |
||||
|
} else { |
||||
|
$var = $key . '=' . $var; |
||||
|
} |
||||
|
} elseif ($key > 0 && $actuallyUseQuery) { |
||||
|
$var = $value['value'] . '=' . $var; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$kvp[$key] = $var; |
||||
|
} |
||||
|
|
||||
|
if (empty($variable)) { |
||||
|
$actuallyUseQuery = false; |
||||
|
} elseif ($value['modifier'] === '*') { |
||||
|
$expanded = implode($joiner, $kvp); |
||||
|
if ($isAssoc) { |
||||
|
// Don't prepend the value name when using the explode |
||||
|
// modifier with an associative array. |
||||
|
$actuallyUseQuery = false; |
||||
|
} |
||||
|
} else { |
||||
|
if ($isAssoc) { |
||||
|
// When an associative array is encountered and the |
||||
|
// explode modifier is not set, then the result must be |
||||
|
// a comma separated list of keys followed by their |
||||
|
// respective values. |
||||
|
foreach ($kvp as $k => &$v) { |
||||
|
$v = $k . ',' . $v; |
||||
|
} |
||||
|
} |
||||
|
$expanded = implode(',', $kvp); |
||||
|
} |
||||
|
} else { |
||||
|
if ($value['modifier'] === ':') { |
||||
|
$variable = substr($variable, 0, $value['position']); |
||||
|
} |
||||
|
$expanded = rawurlencode($variable); |
||||
|
if ($parsed['operator'] === '+' || $parsed['operator'] === '#') { |
||||
|
$expanded = $this->decodeReserved($expanded); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ($actuallyUseQuery) { |
||||
|
if (!$expanded && $joiner !== '&') { |
||||
|
$expanded = $value['value']; |
||||
|
} else { |
||||
|
$expanded = $value['value'] . '=' . $expanded; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$replacements[] = $expanded; |
||||
|
} |
||||
|
|
||||
|
$ret = implode($joiner, $replacements); |
||||
|
if ($ret && $prefix) { |
||||
|
return $prefix . $ret; |
||||
|
} |
||||
|
|
||||
|
return $ret; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Determines if an array is associative. |
||||
|
* |
||||
|
* This makes the assumption that input arrays are sequences or hashes. |
||||
|
* This assumption is a tradeoff for accuracy in favor of speed, but it |
||||
|
* should work in almost every case where input is supplied for a URI |
||||
|
* template. |
||||
|
* |
||||
|
* @param array $array Array to check |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
private function isAssoc(array $array) |
||||
|
{ |
||||
|
return $array && array_keys($array)[0] !== 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Removes percent encoding on reserved characters (used with + and # |
||||
|
* modifiers). |
||||
|
* |
||||
|
* @param string $string String to fix |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
private function decodeReserved($string) |
||||
|
{ |
||||
|
return str_replace(self::$delimsPct, self::$delims, $string); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,333 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp; |
||||
|
|
||||
|
use GuzzleHttp\Handler\CurlHandler; |
||||
|
use GuzzleHttp\Handler\CurlMultiHandler; |
||||
|
use GuzzleHttp\Handler\Proxy; |
||||
|
use GuzzleHttp\Handler\StreamHandler; |
||||
|
|
||||
|
/** |
||||
|
* Expands a URI template |
||||
|
* |
||||
|
* @param string $template URI template |
||||
|
* @param array $variables Template variables |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
function uri_template($template, array $variables) |
||||
|
{ |
||||
|
if (extension_loaded('uri_template')) { |
||||
|
// @codeCoverageIgnoreStart |
||||
|
return \uri_template($template, $variables); |
||||
|
// @codeCoverageIgnoreEnd |
||||
|
} |
||||
|
|
||||
|
static $uriTemplate; |
||||
|
if (!$uriTemplate) { |
||||
|
$uriTemplate = new UriTemplate(); |
||||
|
} |
||||
|
|
||||
|
return $uriTemplate->expand($template, $variables); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Debug function used to describe the provided value type and class. |
||||
|
* |
||||
|
* @param mixed $input |
||||
|
* |
||||
|
* @return string Returns a string containing the type of the variable and |
||||
|
* if a class is provided, the class name. |
||||
|
*/ |
||||
|
function describe_type($input) |
||||
|
{ |
||||
|
switch (gettype($input)) { |
||||
|
case 'object': |
||||
|
return 'object(' . get_class($input) . ')'; |
||||
|
case 'array': |
||||
|
return 'array(' . count($input) . ')'; |
||||
|
default: |
||||
|
ob_start(); |
||||
|
var_dump($input); |
||||
|
// normalize float vs double |
||||
|
return str_replace('double(', 'float(', rtrim(ob_get_clean())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Parses an array of header lines into an associative array of headers. |
||||
|
* |
||||
|
* @param array $lines Header lines array of strings in the following |
||||
|
* format: "Name: Value" |
||||
|
* @return array |
||||
|
*/ |
||||
|
function headers_from_lines($lines) |
||||
|
{ |
||||
|
$headers = []; |
||||
|
|
||||
|
foreach ($lines as $line) { |
||||
|
$parts = explode(':', $line, 2); |
||||
|
$headers[trim($parts[0])][] = isset($parts[1]) |
||||
|
? trim($parts[1]) |
||||
|
: null; |
||||
|
} |
||||
|
|
||||
|
return $headers; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns a debug stream based on the provided variable. |
||||
|
* |
||||
|
* @param mixed $value Optional value |
||||
|
* |
||||
|
* @return resource |
||||
|
*/ |
||||
|
function debug_resource($value = null) |
||||
|
{ |
||||
|
if (is_resource($value)) { |
||||
|
return $value; |
||||
|
} elseif (defined('STDOUT')) { |
||||
|
return STDOUT; |
||||
|
} |
||||
|
|
||||
|
return fopen('php://output', 'w'); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Chooses and creates a default handler to use based on the environment. |
||||
|
* |
||||
|
* The returned handler is not wrapped by any default middlewares. |
||||
|
* |
||||
|
* @throws \RuntimeException if no viable Handler is available. |
||||
|
* @return callable Returns the best handler for the given system. |
||||
|
*/ |
||||
|
function choose_handler() |
||||
|
{ |
||||
|
$handler = null; |
||||
|
if (function_exists('curl_multi_exec') && function_exists('curl_exec')) { |
||||
|
$handler = Proxy::wrapSync(new CurlMultiHandler(), new CurlHandler()); |
||||
|
} elseif (function_exists('curl_exec')) { |
||||
|
$handler = new CurlHandler(); |
||||
|
} elseif (function_exists('curl_multi_exec')) { |
||||
|
$handler = new CurlMultiHandler(); |
||||
|
} |
||||
|
|
||||
|
if (ini_get('allow_url_fopen')) { |
||||
|
$handler = $handler |
||||
|
? Proxy::wrapStreaming($handler, new StreamHandler()) |
||||
|
: new StreamHandler(); |
||||
|
} elseif (!$handler) { |
||||
|
throw new \RuntimeException('GuzzleHttp requires cURL, the ' |
||||
|
. 'allow_url_fopen ini setting, or a custom HTTP handler.'); |
||||
|
} |
||||
|
|
||||
|
return $handler; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the default User-Agent string to use with Guzzle |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
function default_user_agent() |
||||
|
{ |
||||
|
static $defaultAgent = ''; |
||||
|
|
||||
|
if (!$defaultAgent) { |
||||
|
$defaultAgent = 'GuzzleHttp/' . Client::VERSION; |
||||
|
if (extension_loaded('curl') && function_exists('curl_version')) { |
||||
|
$defaultAgent .= ' curl/' . \curl_version()['version']; |
||||
|
} |
||||
|
$defaultAgent .= ' PHP/' . PHP_VERSION; |
||||
|
} |
||||
|
|
||||
|
return $defaultAgent; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the default cacert bundle for the current system. |
||||
|
* |
||||
|
* First, the openssl.cafile and curl.cainfo php.ini settings are checked. |
||||
|
* If those settings are not configured, then the common locations for |
||||
|
* bundles found on Red Hat, CentOS, Fedora, Ubuntu, Debian, FreeBSD, OS X |
||||
|
* and Windows are checked. If any of these file locations are found on |
||||
|
* disk, they will be utilized. |
||||
|
* |
||||
|
* Note: the result of this function is cached for subsequent calls. |
||||
|
* |
||||
|
* @return string |
||||
|
* @throws \RuntimeException if no bundle can be found. |
||||
|
*/ |
||||
|
function default_ca_bundle() |
||||
|
{ |
||||
|
static $cached = null; |
||||
|
static $cafiles = [ |
||||
|
// Red Hat, CentOS, Fedora (provided by the ca-certificates package) |
||||
|
'/etc/pki/tls/certs/ca-bundle.crt', |
||||
|
// Ubuntu, Debian (provided by the ca-certificates package) |
||||
|
'/etc/ssl/certs/ca-certificates.crt', |
||||
|
// FreeBSD (provided by the ca_root_nss package) |
||||
|
'/usr/local/share/certs/ca-root-nss.crt', |
||||
|
// SLES 12 (provided by the ca-certificates package) |
||||
|
'/var/lib/ca-certificates/ca-bundle.pem', |
||||
|
// OS X provided by homebrew (using the default path) |
||||
|
'/usr/local/etc/openssl/cert.pem', |
||||
|
// Google app engine |
||||
|
'/etc/ca-certificates.crt', |
||||
|
// Windows? |
||||
|
'C:\\windows\\system32\\curl-ca-bundle.crt', |
||||
|
'C:\\windows\\curl-ca-bundle.crt', |
||||
|
]; |
||||
|
|
||||
|
if ($cached) { |
||||
|
return $cached; |
||||
|
} |
||||
|
|
||||
|
if ($ca = ini_get('openssl.cafile')) { |
||||
|
return $cached = $ca; |
||||
|
} |
||||
|
|
||||
|
if ($ca = ini_get('curl.cainfo')) { |
||||
|
return $cached = $ca; |
||||
|
} |
||||
|
|
||||
|
foreach ($cafiles as $filename) { |
||||
|
if (file_exists($filename)) { |
||||
|
return $cached = $filename; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
throw new \RuntimeException(<<< EOT |
||||
|
No system CA bundle could be found in any of the the common system locations. |
||||
|
PHP versions earlier than 5.6 are not properly configured to use the system's |
||||
|
CA bundle by default. In order to verify peer certificates, you will need to |
||||
|
supply the path on disk to a certificate bundle to the 'verify' request |
||||
|
option: http://docs.guzzlephp.org/en/latest/clients.html#verify. If you do not |
||||
|
need a specific certificate bundle, then Mozilla provides a commonly used CA |
||||
|
bundle which can be downloaded here (provided by the maintainer of cURL): |
||||
|
https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt. Once |
||||
|
you have a CA bundle available on disk, you can set the 'openssl.cafile' PHP |
||||
|
ini setting to point to the path to the file, allowing you to omit the 'verify' |
||||
|
request option. See http://curl.haxx.se/docs/sslcerts.html for more |
||||
|
information. |
||||
|
EOT |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Creates an associative array of lowercase header names to the actual |
||||
|
* header casing. |
||||
|
* |
||||
|
* @param array $headers |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
function normalize_header_keys(array $headers) |
||||
|
{ |
||||
|
$result = []; |
||||
|
foreach (array_keys($headers) as $key) { |
||||
|
$result[strtolower($key)] = $key; |
||||
|
} |
||||
|
|
||||
|
return $result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns true if the provided host matches any of the no proxy areas. |
||||
|
* |
||||
|
* This method will strip a port from the host if it is present. Each pattern |
||||
|
* can be matched with an exact match (e.g., "foo.com" == "foo.com") or a |
||||
|
* partial match: (e.g., "foo.com" == "baz.foo.com" and ".foo.com" == |
||||
|
* "baz.foo.com", but ".foo.com" != "foo.com"). |
||||
|
* |
||||
|
* Areas are matched in the following cases: |
||||
|
* 1. "*" (without quotes) always matches any hosts. |
||||
|
* 2. An exact match. |
||||
|
* 3. The area starts with "." and the area is the last part of the host. e.g. |
||||
|
* '.mit.edu' will match any host that ends with '.mit.edu'. |
||||
|
* |
||||
|
* @param string $host Host to check against the patterns. |
||||
|
* @param array $noProxyArray An array of host patterns. |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
function is_host_in_noproxy($host, array $noProxyArray) |
||||
|
{ |
||||
|
if (strlen($host) === 0) { |
||||
|
throw new \InvalidArgumentException('Empty host provided'); |
||||
|
} |
||||
|
|
||||
|
// Strip port if present. |
||||
|
if (strpos($host, ':')) { |
||||
|
$host = explode($host, ':', 2)[0]; |
||||
|
} |
||||
|
|
||||
|
foreach ($noProxyArray as $area) { |
||||
|
// Always match on wildcards. |
||||
|
if ($area === '*') { |
||||
|
return true; |
||||
|
} elseif (empty($area)) { |
||||
|
// Don't match on empty values. |
||||
|
continue; |
||||
|
} elseif ($area === $host) { |
||||
|
// Exact matches. |
||||
|
return true; |
||||
|
} else { |
||||
|
// Special match if the area when prefixed with ".". Remove any |
||||
|
// existing leading "." and add a new leading ".". |
||||
|
$area = '.' . ltrim($area, '.'); |
||||
|
if (substr($host, -(strlen($area))) === $area) { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Wrapper for json_decode that throws when an error occurs. |
||||
|
* |
||||
|
* @param string $json JSON data to parse |
||||
|
* @param bool $assoc When true, returned objects will be converted |
||||
|
* into associative arrays. |
||||
|
* @param int $depth User specified recursion depth. |
||||
|
* @param int $options Bitmask of JSON decode options. |
||||
|
* |
||||
|
* @return mixed |
||||
|
* @throws \InvalidArgumentException if the JSON cannot be decoded. |
||||
|
* @link http://www.php.net/manual/en/function.json-decode.php |
||||
|
*/ |
||||
|
function json_decode($json, $assoc = false, $depth = 512, $options = 0) |
||||
|
{ |
||||
|
$data = \json_decode($json, $assoc, $depth, $options); |
||||
|
if (JSON_ERROR_NONE !== json_last_error()) { |
||||
|
throw new \InvalidArgumentException( |
||||
|
'json_decode error: ' . json_last_error_msg() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return $data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Wrapper for JSON encoding that throws when an error occurs. |
||||
|
* |
||||
|
* @param mixed $value The value being encoded |
||||
|
* @param int $options JSON encode option bitmask |
||||
|
* @param int $depth Set the maximum depth. Must be greater than zero. |
||||
|
* |
||||
|
* @return string |
||||
|
* @throws \InvalidArgumentException if the JSON cannot be encoded. |
||||
|
* @link http://www.php.net/manual/en/function.json-encode.php |
||||
|
*/ |
||||
|
function json_encode($value, $options = 0, $depth = 512) |
||||
|
{ |
||||
|
$json = \json_encode($value, $options, $depth); |
||||
|
if (JSON_ERROR_NONE !== json_last_error()) { |
||||
|
throw new \InvalidArgumentException( |
||||
|
'json_encode error: ' . json_last_error_msg() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return $json; |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
<?php |
||||
|
|
||||
|
// Don't redefine the functions if included multiple times. |
||||
|
if (!function_exists('GuzzleHttp\uri_template')) { |
||||
|
require __DIR__ . '/functions.php'; |
||||
|
} |
||||
@ -0,0 +1,65 @@ |
|||||
|
# CHANGELOG |
||||
|
|
||||
|
|
||||
|
## 1.3.1 - 2016-12-20 |
||||
|
|
||||
|
### Fixed |
||||
|
|
||||
|
- `wait()` foreign promise compatibility |
||||
|
|
||||
|
|
||||
|
## 1.3.0 - 2016-11-18 |
||||
|
|
||||
|
### Added |
||||
|
|
||||
|
- Adds support for custom task queues. |
||||
|
|
||||
|
### Fixed |
||||
|
|
||||
|
- Fixed coroutine promise memory leak. |
||||
|
|
||||
|
|
||||
|
## 1.2.0 - 2016-05-18 |
||||
|
|
||||
|
### Changed |
||||
|
|
||||
|
- Update to now catch `\Throwable` on PHP 7+ |
||||
|
|
||||
|
|
||||
|
## 1.1.0 - 2016-03-07 |
||||
|
|
||||
|
### Changed |
||||
|
|
||||
|
- Update EachPromise to prevent recurring on a iterator when advancing, as this |
||||
|
could trigger fatal generator errors. |
||||
|
- Update Promise to allow recursive waiting without unwrapping exceptions. |
||||
|
|
||||
|
|
||||
|
## 1.0.3 - 2015-10-15 |
||||
|
|
||||
|
### Changed |
||||
|
|
||||
|
- Update EachPromise to immediately resolve when the underlying promise iterator |
||||
|
is empty. Previously, such a promise would throw an exception when its `wait` |
||||
|
function was called. |
||||
|
|
||||
|
|
||||
|
## 1.0.2 - 2015-05-15 |
||||
|
|
||||
|
### Changed |
||||
|
|
||||
|
- Conditionally require functions.php. |
||||
|
|
||||
|
|
||||
|
## 1.0.1 - 2015-06-24 |
||||
|
|
||||
|
### Changed |
||||
|
|
||||
|
- Updating EachPromise to call next on the underlying promise iterator as late |
||||
|
as possible to ensure that generators that generate new requests based on |
||||
|
callbacks are not iterated until after callbacks are invoked. |
||||
|
|
||||
|
|
||||
|
## 1.0.0 - 2015-05-12 |
||||
|
|
||||
|
- Initial release |
||||
@ -0,0 +1,19 @@ |
|||||
|
Copyright (c) 2015-2016 Michael Dowling, https://github.com/mtdowling <mtdowling@gmail.com> |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in |
||||
|
all copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
|
THE SOFTWARE. |
||||
@ -0,0 +1,13 @@ |
|||||
|
all: clean test |
||||
|
|
||||
|
test: |
||||
|
vendor/bin/phpunit |
||||
|
|
||||
|
coverage: |
||||
|
vendor/bin/phpunit --coverage-html=artifacts/coverage |
||||
|
|
||||
|
view-coverage: |
||||
|
open artifacts/coverage/index.html |
||||
|
|
||||
|
clean: |
||||
|
rm -rf artifacts/* |
||||
@ -0,0 +1,504 @@ |
|||||
|
# Guzzle Promises |
||||
|
|
||||
|
[Promises/A+](https://promisesaplus.com/) implementation that handles promise |
||||
|
chaining and resolution iteratively, allowing for "infinite" promise chaining |
||||
|
while keeping the stack size constant. Read [this blog post](https://blog.domenic.me/youre-missing-the-point-of-promises/) |
||||
|
for a general introduction to promises. |
||||
|
|
||||
|
- [Features](#features) |
||||
|
- [Quick start](#quick-start) |
||||
|
- [Synchronous wait](#synchronous-wait) |
||||
|
- [Cancellation](#cancellation) |
||||
|
- [API](#api) |
||||
|
- [Promise](#promise) |
||||
|
- [FulfilledPromise](#fulfilledpromise) |
||||
|
- [RejectedPromise](#rejectedpromise) |
||||
|
- [Promise interop](#promise-interop) |
||||
|
- [Implementation notes](#implementation-notes) |
||||
|
|
||||
|
|
||||
|
# Features |
||||
|
|
||||
|
- [Promises/A+](https://promisesaplus.com/) implementation. |
||||
|
- Promise resolution and chaining is handled iteratively, allowing for |
||||
|
"infinite" promise chaining. |
||||
|
- Promises have a synchronous `wait` method. |
||||
|
- Promises can be cancelled. |
||||
|
- Works with any object that has a `then` function. |
||||
|
- C# style async/await coroutine promises using |
||||
|
`GuzzleHttp\Promise\coroutine()`. |
||||
|
|
||||
|
|
||||
|
# Quick start |
||||
|
|
||||
|
A *promise* represents the eventual result of an asynchronous operation. The |
||||
|
primary way of interacting with a promise is through its `then` method, which |
||||
|
registers callbacks to receive either a promise's eventual value or the reason |
||||
|
why the promise cannot be fulfilled. |
||||
|
|
||||
|
|
||||
|
## Callbacks |
||||
|
|
||||
|
Callbacks are registered with the `then` method by providing an optional |
||||
|
`$onFulfilled` followed by an optional `$onRejected` function. |
||||
|
|
||||
|
|
||||
|
```php |
||||
|
use GuzzleHttp\Promise\Promise; |
||||
|
|
||||
|
$promise = new Promise(); |
||||
|
$promise->then( |
||||
|
// $onFulfilled |
||||
|
function ($value) { |
||||
|
echo 'The promise was fulfilled.'; |
||||
|
}, |
||||
|
// $onRejected |
||||
|
function ($reason) { |
||||
|
echo 'The promise was rejected.'; |
||||
|
} |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
*Resolving* a promise means that you either fulfill a promise with a *value* or |
||||
|
reject a promise with a *reason*. Resolving a promises triggers callbacks |
||||
|
registered with the promises's `then` method. These callbacks are triggered |
||||
|
only once and in the order in which they were added. |
||||
|
|
||||
|
|
||||
|
## Resolving a promise |
||||
|
|
||||
|
Promises are fulfilled using the `resolve($value)` method. Resolving a promise |
||||
|
with any value other than a `GuzzleHttp\Promise\RejectedPromise` will trigger |
||||
|
all of the onFulfilled callbacks (resolving a promise with a rejected promise |
||||
|
will reject the promise and trigger the `$onRejected` callbacks). |
||||
|
|
||||
|
```php |
||||
|
use GuzzleHttp\Promise\Promise; |
||||
|
|
||||
|
$promise = new Promise(); |
||||
|
$promise |
||||
|
->then(function ($value) { |
||||
|
// Return a value and don't break the chain |
||||
|
return "Hello, " . $value; |
||||
|
}) |
||||
|
// This then is executed after the first then and receives the value |
||||
|
// returned from the first then. |
||||
|
->then(function ($value) { |
||||
|
echo $value; |
||||
|
}); |
||||
|
|
||||
|
// Resolving the promise triggers the $onFulfilled callbacks and outputs |
||||
|
// "Hello, reader". |
||||
|
$promise->resolve('reader.'); |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
## Promise forwarding |
||||
|
|
||||
|
Promises can be chained one after the other. Each then in the chain is a new |
||||
|
promise. The return value of a promise is what's forwarded to the next |
||||
|
promise in the chain. Returning a promise in a `then` callback will cause the |
||||
|
subsequent promises in the chain to only be fulfilled when the returned promise |
||||
|
has been fulfilled. The next promise in the chain will be invoked with the |
||||
|
resolved value of the promise. |
||||
|
|
||||
|
```php |
||||
|
use GuzzleHttp\Promise\Promise; |
||||
|
|
||||
|
$promise = new Promise(); |
||||
|
$nextPromise = new Promise(); |
||||
|
|
||||
|
$promise |
||||
|
->then(function ($value) use ($nextPromise) { |
||||
|
echo $value; |
||||
|
return $nextPromise; |
||||
|
}) |
||||
|
->then(function ($value) { |
||||
|
echo $value; |
||||
|
}); |
||||
|
|
||||
|
// Triggers the first callback and outputs "A" |
||||
|
$promise->resolve('A'); |
||||
|
// Triggers the second callback and outputs "B" |
||||
|
$nextPromise->resolve('B'); |
||||
|
``` |
||||
|
|
||||
|
## Promise rejection |
||||
|
|
||||
|
When a promise is rejected, the `$onRejected` callbacks are invoked with the |
||||
|
rejection reason. |
||||
|
|
||||
|
```php |
||||
|
use GuzzleHttp\Promise\Promise; |
||||
|
|
||||
|
$promise = new Promise(); |
||||
|
$promise->then(null, function ($reason) { |
||||
|
echo $reason; |
||||
|
}); |
||||
|
|
||||
|
$promise->reject('Error!'); |
||||
|
// Outputs "Error!" |
||||
|
``` |
||||
|
|
||||
|
## Rejection forwarding |
||||
|
|
||||
|
If an exception is thrown in an `$onRejected` callback, subsequent |
||||
|
`$onRejected` callbacks are invoked with the thrown exception as the reason. |
||||
|
|
||||
|
```php |
||||
|
use GuzzleHttp\Promise\Promise; |
||||
|
|
||||
|
$promise = new Promise(); |
||||
|
$promise->then(null, function ($reason) { |
||||
|
throw new \Exception($reason); |
||||
|
})->then(null, function ($reason) { |
||||
|
assert($reason->getMessage() === 'Error!'); |
||||
|
}); |
||||
|
|
||||
|
$promise->reject('Error!'); |
||||
|
``` |
||||
|
|
||||
|
You can also forward a rejection down the promise chain by returning a |
||||
|
`GuzzleHttp\Promise\RejectedPromise` in either an `$onFulfilled` or |
||||
|
`$onRejected` callback. |
||||
|
|
||||
|
```php |
||||
|
use GuzzleHttp\Promise\Promise; |
||||
|
use GuzzleHttp\Promise\RejectedPromise; |
||||
|
|
||||
|
$promise = new Promise(); |
||||
|
$promise->then(null, function ($reason) { |
||||
|
return new RejectedPromise($reason); |
||||
|
})->then(null, function ($reason) { |
||||
|
assert($reason === 'Error!'); |
||||
|
}); |
||||
|
|
||||
|
$promise->reject('Error!'); |
||||
|
``` |
||||
|
|
||||
|
If an exception is not thrown in a `$onRejected` callback and the callback |
||||
|
does not return a rejected promise, downstream `$onFulfilled` callbacks are |
||||
|
invoked using the value returned from the `$onRejected` callback. |
||||
|
|
||||
|
```php |
||||
|
use GuzzleHttp\Promise\Promise; |
||||
|
use GuzzleHttp\Promise\RejectedPromise; |
||||
|
|
||||
|
$promise = new Promise(); |
||||
|
$promise |
||||
|
->then(null, function ($reason) { |
||||
|
return "It's ok"; |
||||
|
}) |
||||
|
->then(function ($value) { |
||||
|
assert($value === "It's ok"); |
||||
|
}); |
||||
|
|
||||
|
$promise->reject('Error!'); |
||||
|
``` |
||||
|
|
||||
|
# Synchronous wait |
||||
|
|
||||
|
You can synchronously force promises to complete using a promise's `wait` |
||||
|
method. When creating a promise, you can provide a wait function that is used |
||||
|
to synchronously force a promise to complete. When a wait function is invoked |
||||
|
it is expected to deliver a value to the promise or reject the promise. If the |
||||
|
wait function does not deliver a value, then an exception is thrown. The wait |
||||
|
function provided to a promise constructor is invoked when the `wait` function |
||||
|
of the promise is called. |
||||
|
|
||||
|
```php |
||||
|
$promise = new Promise(function () use (&$promise) { |
||||
|
$promise->resolve('foo'); |
||||
|
}); |
||||
|
|
||||
|
// Calling wait will return the value of the promise. |
||||
|
echo $promise->wait(); // outputs "foo" |
||||
|
``` |
||||
|
|
||||
|
If an exception is encountered while invoking the wait function of a promise, |
||||
|
the promise is rejected with the exception and the exception is thrown. |
||||
|
|
||||
|
```php |
||||
|
$promise = new Promise(function () use (&$promise) { |
||||
|
throw new \Exception('foo'); |
||||
|
}); |
||||
|
|
||||
|
$promise->wait(); // throws the exception. |
||||
|
``` |
||||
|
|
||||
|
Calling `wait` on a promise that has been fulfilled will not trigger the wait |
||||
|
function. It will simply return the previously resolved value. |
||||
|
|
||||
|
```php |
||||
|
$promise = new Promise(function () { die('this is not called!'); }); |
||||
|
$promise->resolve('foo'); |
||||
|
echo $promise->wait(); // outputs "foo" |
||||
|
``` |
||||
|
|
||||
|
Calling `wait` on a promise that has been rejected will throw an exception. If |
||||
|
the rejection reason is an instance of `\Exception` the reason is thrown. |
||||
|
Otherwise, a `GuzzleHttp\Promise\RejectionException` is thrown and the reason |
||||
|
can be obtained by calling the `getReason` method of the exception. |
||||
|
|
||||
|
```php |
||||
|
$promise = new Promise(); |
||||
|
$promise->reject('foo'); |
||||
|
$promise->wait(); |
||||
|
``` |
||||
|
|
||||
|
> PHP Fatal error: Uncaught exception 'GuzzleHttp\Promise\RejectionException' with message 'The promise was rejected with value: foo' |
||||
|
|
||||
|
|
||||
|
## Unwrapping a promise |
||||
|
|
||||
|
When synchronously waiting on a promise, you are joining the state of the |
||||
|
promise into the current state of execution (i.e., return the value of the |
||||
|
promise if it was fulfilled or throw an exception if it was rejected). This is |
||||
|
called "unwrapping" the promise. Waiting on a promise will by default unwrap |
||||
|
the promise state. |
||||
|
|
||||
|
You can force a promise to resolve and *not* unwrap the state of the promise |
||||
|
by passing `false` to the first argument of the `wait` function: |
||||
|
|
||||
|
```php |
||||
|
$promise = new Promise(); |
||||
|
$promise->reject('foo'); |
||||
|
// This will not throw an exception. It simply ensures the promise has |
||||
|
// been resolved. |
||||
|
$promise->wait(false); |
||||
|
``` |
||||
|
|
||||
|
When unwrapping a promise, the resolved value of the promise will be waited |
||||
|
upon until the unwrapped value is not a promise. This means that if you resolve |
||||
|
promise A with a promise B and unwrap promise A, the value returned by the |
||||
|
wait function will be the value delivered to promise B. |
||||
|
|
||||
|
**Note**: when you do not unwrap the promise, no value is returned. |
||||
|
|
||||
|
|
||||
|
# Cancellation |
||||
|
|
||||
|
You can cancel a promise that has not yet been fulfilled using the `cancel()` |
||||
|
method of a promise. When creating a promise you can provide an optional |
||||
|
cancel function that when invoked cancels the action of computing a resolution |
||||
|
of the promise. |
||||
|
|
||||
|
|
||||
|
# API |
||||
|
|
||||
|
|
||||
|
## Promise |
||||
|
|
||||
|
When creating a promise object, you can provide an optional `$waitFn` and |
||||
|
`$cancelFn`. `$waitFn` is a function that is invoked with no arguments and is |
||||
|
expected to resolve the promise. `$cancelFn` is a function with no arguments |
||||
|
that is expected to cancel the computation of a promise. It is invoked when the |
||||
|
`cancel()` method of a promise is called. |
||||
|
|
||||
|
```php |
||||
|
use GuzzleHttp\Promise\Promise; |
||||
|
|
||||
|
$promise = new Promise( |
||||
|
function () use (&$promise) { |
||||
|
$promise->resolve('waited'); |
||||
|
}, |
||||
|
function () { |
||||
|
// do something that will cancel the promise computation (e.g., close |
||||
|
// a socket, cancel a database query, etc...) |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
assert('waited' === $promise->wait()); |
||||
|
``` |
||||
|
|
||||
|
A promise has the following methods: |
||||
|
|
||||
|
- `then(callable $onFulfilled, callable $onRejected) : PromiseInterface` |
||||
|
|
||||
|
Appends fulfillment and rejection handlers to the promise, and returns a new promise resolving to the return value of the called handler. |
||||
|
|
||||
|
- `otherwise(callable $onRejected) : PromiseInterface` |
||||
|
|
||||
|
Appends a rejection handler callback to the promise, and returns a new promise resolving to the return value of the callback if it is called, or to its original fulfillment value if the promise is instead fulfilled. |
||||
|
|
||||
|
- `wait($unwrap = true) : mixed` |
||||
|
|
||||
|
Synchronously waits on the promise to complete. |
||||
|
|
||||
|
`$unwrap` controls whether or not the value of the promise is returned for a |
||||
|
fulfilled promise or if an exception is thrown if the promise is rejected. |
||||
|
This is set to `true` by default. |
||||
|
|
||||
|
- `cancel()` |
||||
|
|
||||
|
Attempts to cancel the promise if possible. The promise being cancelled and |
||||
|
the parent most ancestor that has not yet been resolved will also be |
||||
|
cancelled. Any promises waiting on the cancelled promise to resolve will also |
||||
|
be cancelled. |
||||
|
|
||||
|
- `getState() : string` |
||||
|
|
||||
|
Returns the state of the promise. One of `pending`, `fulfilled`, or |
||||
|
`rejected`. |
||||
|
|
||||
|
- `resolve($value)` |
||||
|
|
||||
|
Fulfills the promise with the given `$value`. |
||||
|
|
||||
|
- `reject($reason)` |
||||
|
|
||||
|
Rejects the promise with the given `$reason`. |
||||
|
|
||||
|
|
||||
|
## FulfilledPromise |
||||
|
|
||||
|
A fulfilled promise can be created to represent a promise that has been |
||||
|
fulfilled. |
||||
|
|
||||
|
```php |
||||
|
use GuzzleHttp\Promise\FulfilledPromise; |
||||
|
|
||||
|
$promise = new FulfilledPromise('value'); |
||||
|
|
||||
|
// Fulfilled callbacks are immediately invoked. |
||||
|
$promise->then(function ($value) { |
||||
|
echo $value; |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
## RejectedPromise |
||||
|
|
||||
|
A rejected promise can be created to represent a promise that has been |
||||
|
rejected. |
||||
|
|
||||
|
```php |
||||
|
use GuzzleHttp\Promise\RejectedPromise; |
||||
|
|
||||
|
$promise = new RejectedPromise('Error'); |
||||
|
|
||||
|
// Rejected callbacks are immediately invoked. |
||||
|
$promise->then(null, function ($reason) { |
||||
|
echo $reason; |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
# Promise interop |
||||
|
|
||||
|
This library works with foreign promises that have a `then` method. This means |
||||
|
you can use Guzzle promises with [React promises](https://github.com/reactphp/promise) |
||||
|
for example. When a foreign promise is returned inside of a then method |
||||
|
callback, promise resolution will occur recursively. |
||||
|
|
||||
|
```php |
||||
|
// Create a React promise |
||||
|
$deferred = new React\Promise\Deferred(); |
||||
|
$reactPromise = $deferred->promise(); |
||||
|
|
||||
|
// Create a Guzzle promise that is fulfilled with a React promise. |
||||
|
$guzzlePromise = new \GuzzleHttp\Promise\Promise(); |
||||
|
$guzzlePromise->then(function ($value) use ($reactPromise) { |
||||
|
// Do something something with the value... |
||||
|
// Return the React promise |
||||
|
return $reactPromise; |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
Please note that wait and cancel chaining is no longer possible when forwarding |
||||
|
a foreign promise. You will need to wrap a third-party promise with a Guzzle |
||||
|
promise in order to utilize wait and cancel functions with foreign promises. |
||||
|
|
||||
|
|
||||
|
## Event Loop Integration |
||||
|
|
||||
|
In order to keep the stack size constant, Guzzle promises are resolved |
||||
|
asynchronously using a task queue. When waiting on promises synchronously, the |
||||
|
task queue will be automatically run to ensure that the blocking promise and |
||||
|
any forwarded promises are resolved. When using promises asynchronously in an |
||||
|
event loop, you will need to run the task queue on each tick of the loop. If |
||||
|
you do not run the task queue, then promises will not be resolved. |
||||
|
|
||||
|
You can run the task queue using the `run()` method of the global task queue |
||||
|
instance. |
||||
|
|
||||
|
```php |
||||
|
// Get the global task queue |
||||
|
$queue = \GuzzleHttp\Promise\queue(); |
||||
|
$queue->run(); |
||||
|
``` |
||||
|
|
||||
|
For example, you could use Guzzle promises with React using a periodic timer: |
||||
|
|
||||
|
```php |
||||
|
$loop = React\EventLoop\Factory::create(); |
||||
|
$loop->addPeriodicTimer(0, [$queue, 'run']); |
||||
|
``` |
||||
|
|
||||
|
*TODO*: Perhaps adding a `futureTick()` on each tick would be faster? |
||||
|
|
||||
|
|
||||
|
# Implementation notes |
||||
|
|
||||
|
|
||||
|
## Promise resolution and chaining is handled iteratively |
||||
|
|
||||
|
By shuffling pending handlers from one owner to another, promises are |
||||
|
resolved iteratively, allowing for "infinite" then chaining. |
||||
|
|
||||
|
```php |
||||
|
<?php |
||||
|
require 'vendor/autoload.php'; |
||||
|
|
||||
|
use GuzzleHttp\Promise\Promise; |
||||
|
|
||||
|
$parent = new Promise(); |
||||
|
$p = $parent; |
||||
|
|
||||
|
for ($i = 0; $i < 1000; $i++) { |
||||
|
$p = $p->then(function ($v) { |
||||
|
// The stack size remains constant (a good thing) |
||||
|
echo xdebug_get_stack_depth() . ', '; |
||||
|
return $v + 1; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
$parent->resolve(0); |
||||
|
var_dump($p->wait()); // int(1000) |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
When a promise is fulfilled or rejected with a non-promise value, the promise |
||||
|
then takes ownership of the handlers of each child promise and delivers values |
||||
|
down the chain without using recursion. |
||||
|
|
||||
|
When a promise is resolved with another promise, the original promise transfers |
||||
|
all of its pending handlers to the new promise. When the new promise is |
||||
|
eventually resolved, all of the pending handlers are delivered the forwarded |
||||
|
value. |
||||
|
|
||||
|
|
||||
|
## A promise is the deferred. |
||||
|
|
||||
|
Some promise libraries implement promises using a deferred object to represent |
||||
|
a computation and a promise object to represent the delivery of the result of |
||||
|
the computation. This is a nice separation of computation and delivery because |
||||
|
consumers of the promise cannot modify the value that will be eventually |
||||
|
delivered. |
||||
|
|
||||
|
One side effect of being able to implement promise resolution and chaining |
||||
|
iteratively is that you need to be able for one promise to reach into the state |
||||
|
of another promise to shuffle around ownership of handlers. In order to achieve |
||||
|
this without making the handlers of a promise publicly mutable, a promise is |
||||
|
also the deferred value, allowing promises of the same parent class to reach |
||||
|
into and modify the private properties of promises of the same type. While this |
||||
|
does allow consumers of the value to modify the resolution or rejection of the |
||||
|
deferred, it is a small price to pay for keeping the stack size constant. |
||||
|
|
||||
|
```php |
||||
|
$promise = new Promise(); |
||||
|
$promise->then(function ($value) { echo $value; }); |
||||
|
// The promise is the deferred value, so you can deliver a value to it. |
||||
|
$promise->resolve('foo'); |
||||
|
// prints "foo" |
||||
|
``` |
||||
@ -0,0 +1,34 @@ |
|||||
|
{ |
||||
|
"name": "guzzlehttp/promises", |
||||
|
"description": "Guzzle promises library", |
||||
|
"keywords": ["promise"], |
||||
|
"license": "MIT", |
||||
|
"authors": [ |
||||
|
{ |
||||
|
"name": "Michael Dowling", |
||||
|
"email": "mtdowling@gmail.com", |
||||
|
"homepage": "https://github.com/mtdowling" |
||||
|
} |
||||
|
], |
||||
|
"require": { |
||||
|
"php": ">=5.5.0" |
||||
|
}, |
||||
|
"require-dev": { |
||||
|
"phpunit/phpunit": "^4.0" |
||||
|
}, |
||||
|
"autoload": { |
||||
|
"psr-4": { |
||||
|
"GuzzleHttp\\Promise\\": "src/" |
||||
|
}, |
||||
|
"files": ["src/functions_include.php"] |
||||
|
}, |
||||
|
"scripts": { |
||||
|
"test": "vendor/bin/phpunit", |
||||
|
"test-ci": "vendor/bin/phpunit --coverage-text" |
||||
|
}, |
||||
|
"extra": { |
||||
|
"branch-alias": { |
||||
|
"dev-master": "1.4-dev" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Promise; |
||||
|
|
||||
|
/** |
||||
|
* Exception thrown when too many errors occur in the some() or any() methods. |
||||
|
*/ |
||||
|
class AggregateException extends RejectionException |
||||
|
{ |
||||
|
public function __construct($msg, array $reasons) |
||||
|
{ |
||||
|
parent::__construct( |
||||
|
$reasons, |
||||
|
sprintf('%s; %d rejected promises', $msg, count($reasons)) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Promise; |
||||
|
|
||||
|
/** |
||||
|
* Exception that is set as the reason for a promise that has been cancelled. |
||||
|
*/ |
||||
|
class CancellationException extends RejectionException |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,151 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Promise; |
||||
|
|
||||
|
use Exception; |
||||
|
use Generator; |
||||
|
use Throwable; |
||||
|
|
||||
|
/** |
||||
|
* Creates a promise that is resolved using a generator that yields values or |
||||
|
* promises (somewhat similar to C#'s async keyword). |
||||
|
* |
||||
|
* When called, the coroutine function will start an instance of the generator |
||||
|
* and returns a promise that is fulfilled with its final yielded value. |
||||
|
* |
||||
|
* Control is returned back to the generator when the yielded promise settles. |
||||
|
* This can lead to less verbose code when doing lots of sequential async calls |
||||
|
* with minimal processing in between. |
||||
|
* |
||||
|
* use GuzzleHttp\Promise; |
||||
|
* |
||||
|
* function createPromise($value) { |
||||
|
* return new Promise\FulfilledPromise($value); |
||||
|
* } |
||||
|
* |
||||
|
* $promise = Promise\coroutine(function () { |
||||
|
* $value = (yield createPromise('a')); |
||||
|
* try { |
||||
|
* $value = (yield createPromise($value . 'b')); |
||||
|
* } catch (\Exception $e) { |
||||
|
* // The promise was rejected. |
||||
|
* } |
||||
|
* yield $value . 'c'; |
||||
|
* }); |
||||
|
* |
||||
|
* // Outputs "abc" |
||||
|
* $promise->then(function ($v) { echo $v; }); |
||||
|
* |
||||
|
* @param callable $generatorFn Generator function to wrap into a promise. |
||||
|
* |
||||
|
* @return Promise |
||||
|
* @link https://github.com/petkaantonov/bluebird/blob/master/API.md#generators inspiration |
||||
|
*/ |
||||
|
final class Coroutine implements PromiseInterface |
||||
|
{ |
||||
|
/** |
||||
|
* @var PromiseInterface|null |
||||
|
*/ |
||||
|
private $currentPromise; |
||||
|
|
||||
|
/** |
||||
|
* @var Generator |
||||
|
*/ |
||||
|
private $generator; |
||||
|
|
||||
|
/** |
||||
|
* @var Promise |
||||
|
*/ |
||||
|
private $result; |
||||
|
|
||||
|
public function __construct(callable $generatorFn) |
||||
|
{ |
||||
|
$this->generator = $generatorFn(); |
||||
|
$this->result = new Promise(function () { |
||||
|
while (isset($this->currentPromise)) { |
||||
|
$this->currentPromise->wait(); |
||||
|
} |
||||
|
}); |
||||
|
$this->nextCoroutine($this->generator->current()); |
||||
|
} |
||||
|
|
||||
|
public function then( |
||||
|
callable $onFulfilled = null, |
||||
|
callable $onRejected = null |
||||
|
) { |
||||
|
return $this->result->then($onFulfilled, $onRejected); |
||||
|
} |
||||
|
|
||||
|
public function otherwise(callable $onRejected) |
||||
|
{ |
||||
|
return $this->result->otherwise($onRejected); |
||||
|
} |
||||
|
|
||||
|
public function wait($unwrap = true) |
||||
|
{ |
||||
|
return $this->result->wait($unwrap); |
||||
|
} |
||||
|
|
||||
|
public function getState() |
||||
|
{ |
||||
|
return $this->result->getState(); |
||||
|
} |
||||
|
|
||||
|
public function resolve($value) |
||||
|
{ |
||||
|
$this->result->resolve($value); |
||||
|
} |
||||
|
|
||||
|
public function reject($reason) |
||||
|
{ |
||||
|
$this->result->reject($reason); |
||||
|
} |
||||
|
|
||||
|
public function cancel() |
||||
|
{ |
||||
|
$this->currentPromise->cancel(); |
||||
|
$this->result->cancel(); |
||||
|
} |
||||
|
|
||||
|
private function nextCoroutine($yielded) |
||||
|
{ |
||||
|
$this->currentPromise = promise_for($yielded) |
||||
|
->then([$this, '_handleSuccess'], [$this, '_handleFailure']); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @internal |
||||
|
*/ |
||||
|
public function _handleSuccess($value) |
||||
|
{ |
||||
|
unset($this->currentPromise); |
||||
|
try { |
||||
|
$next = $this->generator->send($value); |
||||
|
if ($this->generator->valid()) { |
||||
|
$this->nextCoroutine($next); |
||||
|
} else { |
||||
|
$this->result->resolve($value); |
||||
|
} |
||||
|
} catch (Exception $exception) { |
||||
|
$this->result->reject($exception); |
||||
|
} catch (Throwable $throwable) { |
||||
|
$this->result->reject($throwable); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @internal |
||||
|
*/ |
||||
|
public function _handleFailure($reason) |
||||
|
{ |
||||
|
unset($this->currentPromise); |
||||
|
try { |
||||
|
$nextYield = $this->generator->throw(exception_for($reason)); |
||||
|
// The throw was caught, so keep iterating on the coroutine |
||||
|
$this->nextCoroutine($nextYield); |
||||
|
} catch (Exception $exception) { |
||||
|
$this->result->reject($exception); |
||||
|
} catch (Throwable $throwable) { |
||||
|
$this->result->reject($throwable); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,229 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Promise; |
||||
|
|
||||
|
/** |
||||
|
* Represents a promise that iterates over many promises and invokes |
||||
|
* side-effect functions in the process. |
||||
|
*/ |
||||
|
class EachPromise implements PromisorInterface |
||||
|
{ |
||||
|
private $pending = []; |
||||
|
|
||||
|
/** @var \Iterator */ |
||||
|
private $iterable; |
||||
|
|
||||
|
/** @var callable|int */ |
||||
|
private $concurrency; |
||||
|
|
||||
|
/** @var callable */ |
||||
|
private $onFulfilled; |
||||
|
|
||||
|
/** @var callable */ |
||||
|
private $onRejected; |
||||
|
|
||||
|
/** @var Promise */ |
||||
|
private $aggregate; |
||||
|
|
||||
|
/** @var bool */ |
||||
|
private $mutex; |
||||
|
|
||||
|
/** |
||||
|
* Configuration hash can include the following key value pairs: |
||||
|
* |
||||
|
* - fulfilled: (callable) Invoked when a promise fulfills. The function |
||||
|
* is invoked with three arguments: the fulfillment value, the index |
||||
|
* position from the iterable list of the promise, and the aggregate |
||||
|
* promise that manages all of the promises. The aggregate promise may |
||||
|
* be resolved from within the callback to short-circuit the promise. |
||||
|
* - rejected: (callable) Invoked when a promise is rejected. The |
||||
|
* function is invoked with three arguments: the rejection reason, the |
||||
|
* index position from the iterable list of the promise, and the |
||||
|
* aggregate promise that manages all of the promises. The aggregate |
||||
|
* promise may be resolved from within the callback to short-circuit |
||||
|
* the promise. |
||||
|
* - concurrency: (integer) Pass this configuration option to limit the |
||||
|
* allowed number of outstanding concurrently executing promises, |
||||
|
* creating a capped pool of promises. There is no limit by default. |
||||
|
* |
||||
|
* @param mixed $iterable Promises or values to iterate. |
||||
|
* @param array $config Configuration options |
||||
|
*/ |
||||
|
public function __construct($iterable, array $config = []) |
||||
|
{ |
||||
|
$this->iterable = iter_for($iterable); |
||||
|
|
||||
|
if (isset($config['concurrency'])) { |
||||
|
$this->concurrency = $config['concurrency']; |
||||
|
} |
||||
|
|
||||
|
if (isset($config['fulfilled'])) { |
||||
|
$this->onFulfilled = $config['fulfilled']; |
||||
|
} |
||||
|
|
||||
|
if (isset($config['rejected'])) { |
||||
|
$this->onRejected = $config['rejected']; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function promise() |
||||
|
{ |
||||
|
if ($this->aggregate) { |
||||
|
return $this->aggregate; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
$this->createPromise(); |
||||
|
$this->iterable->rewind(); |
||||
|
$this->refillPending(); |
||||
|
} catch (\Throwable $e) { |
||||
|
$this->aggregate->reject($e); |
||||
|
} catch (\Exception $e) { |
||||
|
$this->aggregate->reject($e); |
||||
|
} |
||||
|
|
||||
|
return $this->aggregate; |
||||
|
} |
||||
|
|
||||
|
private function createPromise() |
||||
|
{ |
||||
|
$this->mutex = false; |
||||
|
$this->aggregate = new Promise(function () { |
||||
|
reset($this->pending); |
||||
|
if (empty($this->pending) && !$this->iterable->valid()) { |
||||
|
$this->aggregate->resolve(null); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Consume a potentially fluctuating list of promises while |
||||
|
// ensuring that indexes are maintained (precluding array_shift). |
||||
|
while ($promise = current($this->pending)) { |
||||
|
next($this->pending); |
||||
|
$promise->wait(); |
||||
|
if ($this->aggregate->getState() !== PromiseInterface::PENDING) { |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Clear the references when the promise is resolved. |
||||
|
$clearFn = function () { |
||||
|
$this->iterable = $this->concurrency = $this->pending = null; |
||||
|
$this->onFulfilled = $this->onRejected = null; |
||||
|
}; |
||||
|
|
||||
|
$this->aggregate->then($clearFn, $clearFn); |
||||
|
} |
||||
|
|
||||
|
private function refillPending() |
||||
|
{ |
||||
|
if (!$this->concurrency) { |
||||
|
// Add all pending promises. |
||||
|
while ($this->addPending() && $this->advanceIterator()); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Add only up to N pending promises. |
||||
|
$concurrency = is_callable($this->concurrency) |
||||
|
? call_user_func($this->concurrency, count($this->pending)) |
||||
|
: $this->concurrency; |
||||
|
$concurrency = max($concurrency - count($this->pending), 0); |
||||
|
// Concurrency may be set to 0 to disallow new promises. |
||||
|
if (!$concurrency) { |
||||
|
return; |
||||
|
} |
||||
|
// Add the first pending promise. |
||||
|
$this->addPending(); |
||||
|
// Note this is special handling for concurrency=1 so that we do |
||||
|
// not advance the iterator after adding the first promise. This |
||||
|
// helps work around issues with generators that might not have the |
||||
|
// next value to yield until promise callbacks are called. |
||||
|
while (--$concurrency |
||||
|
&& $this->advanceIterator() |
||||
|
&& $this->addPending()); |
||||
|
} |
||||
|
|
||||
|
private function addPending() |
||||
|
{ |
||||
|
if (!$this->iterable || !$this->iterable->valid()) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
$promise = promise_for($this->iterable->current()); |
||||
|
$idx = $this->iterable->key(); |
||||
|
|
||||
|
$this->pending[$idx] = $promise->then( |
||||
|
function ($value) use ($idx) { |
||||
|
if ($this->onFulfilled) { |
||||
|
call_user_func( |
||||
|
$this->onFulfilled, $value, $idx, $this->aggregate |
||||
|
); |
||||
|
} |
||||
|
$this->step($idx); |
||||
|
}, |
||||
|
function ($reason) use ($idx) { |
||||
|
if ($this->onRejected) { |
||||
|
call_user_func( |
||||
|
$this->onRejected, $reason, $idx, $this->aggregate |
||||
|
); |
||||
|
} |
||||
|
$this->step($idx); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private function advanceIterator() |
||||
|
{ |
||||
|
// Place a lock on the iterator so that we ensure to not recurse, |
||||
|
// preventing fatal generator errors. |
||||
|
if ($this->mutex) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
$this->mutex = true; |
||||
|
|
||||
|
try { |
||||
|
$this->iterable->next(); |
||||
|
$this->mutex = false; |
||||
|
return true; |
||||
|
} catch (\Throwable $e) { |
||||
|
$this->aggregate->reject($e); |
||||
|
$this->mutex = false; |
||||
|
return false; |
||||
|
} catch (\Exception $e) { |
||||
|
$this->aggregate->reject($e); |
||||
|
$this->mutex = false; |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function step($idx) |
||||
|
{ |
||||
|
// If the promise was already resolved, then ignore this step. |
||||
|
if ($this->aggregate->getState() !== PromiseInterface::PENDING) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
unset($this->pending[$idx]); |
||||
|
|
||||
|
// Only refill pending promises if we are not locked, preventing the |
||||
|
// EachPromise to recursively invoke the provided iterator, which |
||||
|
// cause a fatal error: "Cannot resume an already running generator" |
||||
|
if ($this->advanceIterator() && !$this->checkIfFinished()) { |
||||
|
// Add more pending promises if possible. |
||||
|
$this->refillPending(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function checkIfFinished() |
||||
|
{ |
||||
|
if (!$this->pending && !$this->iterable->valid()) { |
||||
|
// Resolve the promise if there's nothing left to do. |
||||
|
$this->aggregate->resolve(null); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,82 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Promise; |
||||
|
|
||||
|
/** |
||||
|
* A promise that has been fulfilled. |
||||
|
* |
||||
|
* Thenning off of this promise will invoke the onFulfilled callback |
||||
|
* immediately and ignore other callbacks. |
||||
|
*/ |
||||
|
class FulfilledPromise implements PromiseInterface |
||||
|
{ |
||||
|
private $value; |
||||
|
|
||||
|
public function __construct($value) |
||||
|
{ |
||||
|
if (method_exists($value, 'then')) { |
||||
|
throw new \InvalidArgumentException( |
||||
|
'You cannot create a FulfilledPromise with a promise.'); |
||||
|
} |
||||
|
|
||||
|
$this->value = $value; |
||||
|
} |
||||
|
|
||||
|
public function then( |
||||
|
callable $onFulfilled = null, |
||||
|
callable $onRejected = null |
||||
|
) { |
||||
|
// Return itself if there is no onFulfilled function. |
||||
|
if (!$onFulfilled) { |
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
$queue = queue(); |
||||
|
$p = new Promise([$queue, 'run']); |
||||
|
$value = $this->value; |
||||
|
$queue->add(static function () use ($p, $value, $onFulfilled) { |
||||
|
if ($p->getState() === self::PENDING) { |
||||
|
try { |
||||
|
$p->resolve($onFulfilled($value)); |
||||
|
} catch (\Throwable $e) { |
||||
|
$p->reject($e); |
||||
|
} catch (\Exception $e) { |
||||
|
$p->reject($e); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return $p; |
||||
|
} |
||||
|
|
||||
|
public function otherwise(callable $onRejected) |
||||
|
{ |
||||
|
return $this->then(null, $onRejected); |
||||
|
} |
||||
|
|
||||
|
public function wait($unwrap = true, $defaultDelivery = null) |
||||
|
{ |
||||
|
return $unwrap ? $this->value : null; |
||||
|
} |
||||
|
|
||||
|
public function getState() |
||||
|
{ |
||||
|
return self::FULFILLED; |
||||
|
} |
||||
|
|
||||
|
public function resolve($value) |
||||
|
{ |
||||
|
if ($value !== $this->value) { |
||||
|
throw new \LogicException("Cannot resolve a fulfilled promise"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function reject($reason) |
||||
|
{ |
||||
|
throw new \LogicException("Cannot reject a fulfilled promise"); |
||||
|
} |
||||
|
|
||||
|
public function cancel() |
||||
|
{ |
||||
|
// pass |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,280 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Promise; |
||||
|
|
||||
|
/** |
||||
|
* Promises/A+ implementation that avoids recursion when possible. |
||||
|
* |
||||
|
* @link https://promisesaplus.com/ |
||||
|
*/ |
||||
|
class Promise implements PromiseInterface |
||||
|
{ |
||||
|
private $state = self::PENDING; |
||||
|
private $result; |
||||
|
private $cancelFn; |
||||
|
private $waitFn; |
||||
|
private $waitList; |
||||
|
private $handlers = []; |
||||
|
|
||||
|
/** |
||||
|
* @param callable $waitFn Fn that when invoked resolves the promise. |
||||
|
* @param callable $cancelFn Fn that when invoked cancels the promise. |
||||
|
*/ |
||||
|
public function __construct( |
||||
|
callable $waitFn = null, |
||||
|
callable $cancelFn = null |
||||
|
) { |
||||
|
$this->waitFn = $waitFn; |
||||
|
$this->cancelFn = $cancelFn; |
||||
|
} |
||||
|
|
||||
|
public function then( |
||||
|
callable $onFulfilled = null, |
||||
|
callable $onRejected = null |
||||
|
) { |
||||
|
if ($this->state === self::PENDING) { |
||||
|
$p = new Promise(null, [$this, 'cancel']); |
||||
|
$this->handlers[] = [$p, $onFulfilled, $onRejected]; |
||||
|
$p->waitList = $this->waitList; |
||||
|
$p->waitList[] = $this; |
||||
|
return $p; |
||||
|
} |
||||
|
|
||||
|
// Return a fulfilled promise and immediately invoke any callbacks. |
||||
|
if ($this->state === self::FULFILLED) { |
||||
|
return $onFulfilled |
||||
|
? promise_for($this->result)->then($onFulfilled) |
||||
|
: promise_for($this->result); |
||||
|
} |
||||
|
|
||||
|
// It's either cancelled or rejected, so return a rejected promise |
||||
|
// and immediately invoke any callbacks. |
||||
|
$rejection = rejection_for($this->result); |
||||
|
return $onRejected ? $rejection->then(null, $onRejected) : $rejection; |
||||
|
} |
||||
|
|
||||
|
public function otherwise(callable $onRejected) |
||||
|
{ |
||||
|
return $this->then(null, $onRejected); |
||||
|
} |
||||
|
|
||||
|
public function wait($unwrap = true) |
||||
|
{ |
||||
|
$this->waitIfPending(); |
||||
|
|
||||
|
$inner = $this->result instanceof PromiseInterface |
||||
|
? $this->result->wait($unwrap) |
||||
|
: $this->result; |
||||
|
|
||||
|
if ($unwrap) { |
||||
|
if ($this->result instanceof PromiseInterface |
||||
|
|| $this->state === self::FULFILLED |
||||
|
) { |
||||
|
return $inner; |
||||
|
} else { |
||||
|
// It's rejected so "unwrap" and throw an exception. |
||||
|
throw exception_for($inner); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function getState() |
||||
|
{ |
||||
|
return $this->state; |
||||
|
} |
||||
|
|
||||
|
public function cancel() |
||||
|
{ |
||||
|
if ($this->state !== self::PENDING) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
$this->waitFn = $this->waitList = null; |
||||
|
|
||||
|
if ($this->cancelFn) { |
||||
|
$fn = $this->cancelFn; |
||||
|
$this->cancelFn = null; |
||||
|
try { |
||||
|
$fn(); |
||||
|
} catch (\Throwable $e) { |
||||
|
$this->reject($e); |
||||
|
} catch (\Exception $e) { |
||||
|
$this->reject($e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Reject the promise only if it wasn't rejected in a then callback. |
||||
|
if ($this->state === self::PENDING) { |
||||
|
$this->reject(new CancellationException('Promise has been cancelled')); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function resolve($value) |
||||
|
{ |
||||
|
$this->settle(self::FULFILLED, $value); |
||||
|
} |
||||
|
|
||||
|
public function reject($reason) |
||||
|
{ |
||||
|
$this->settle(self::REJECTED, $reason); |
||||
|
} |
||||
|
|
||||
|
private function settle($state, $value) |
||||
|
{ |
||||
|
if ($this->state !== self::PENDING) { |
||||
|
// Ignore calls with the same resolution. |
||||
|
if ($state === $this->state && $value === $this->result) { |
||||
|
return; |
||||
|
} |
||||
|
throw $this->state === $state |
||||
|
? new \LogicException("The promise is already {$state}.") |
||||
|
: new \LogicException("Cannot change a {$this->state} promise to {$state}"); |
||||
|
} |
||||
|
|
||||
|
if ($value === $this) { |
||||
|
throw new \LogicException('Cannot fulfill or reject a promise with itself'); |
||||
|
} |
||||
|
|
||||
|
// Clear out the state of the promise but stash the handlers. |
||||
|
$this->state = $state; |
||||
|
$this->result = $value; |
||||
|
$handlers = $this->handlers; |
||||
|
$this->handlers = null; |
||||
|
$this->waitList = $this->waitFn = null; |
||||
|
$this->cancelFn = null; |
||||
|
|
||||
|
if (!$handlers) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// If the value was not a settled promise or a thenable, then resolve |
||||
|
// it in the task queue using the correct ID. |
||||
|
if (!method_exists($value, 'then')) { |
||||
|
$id = $state === self::FULFILLED ? 1 : 2; |
||||
|
// It's a success, so resolve the handlers in the queue. |
||||
|
queue()->add(static function () use ($id, $value, $handlers) { |
||||
|
foreach ($handlers as $handler) { |
||||
|
self::callHandler($id, $value, $handler); |
||||
|
} |
||||
|
}); |
||||
|
} elseif ($value instanceof Promise |
||||
|
&& $value->getState() === self::PENDING |
||||
|
) { |
||||
|
// We can just merge our handlers onto the next promise. |
||||
|
$value->handlers = array_merge($value->handlers, $handlers); |
||||
|
} else { |
||||
|
// Resolve the handlers when the forwarded promise is resolved. |
||||
|
$value->then( |
||||
|
static function ($value) use ($handlers) { |
||||
|
foreach ($handlers as $handler) { |
||||
|
self::callHandler(1, $value, $handler); |
||||
|
} |
||||
|
}, |
||||
|
static function ($reason) use ($handlers) { |
||||
|
foreach ($handlers as $handler) { |
||||
|
self::callHandler(2, $reason, $handler); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Call a stack of handlers using a specific callback index and value. |
||||
|
* |
||||
|
* @param int $index 1 (resolve) or 2 (reject). |
||||
|
* @param mixed $value Value to pass to the callback. |
||||
|
* @param array $handler Array of handler data (promise and callbacks). |
||||
|
* |
||||
|
* @return array Returns the next group to resolve. |
||||
|
*/ |
||||
|
private static function callHandler($index, $value, array $handler) |
||||
|
{ |
||||
|
/** @var PromiseInterface $promise */ |
||||
|
$promise = $handler[0]; |
||||
|
|
||||
|
// The promise may have been cancelled or resolved before placing |
||||
|
// this thunk in the queue. |
||||
|
if ($promise->getState() !== self::PENDING) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
if (isset($handler[$index])) { |
||||
|
$promise->resolve($handler[$index]($value)); |
||||
|
} elseif ($index === 1) { |
||||
|
// Forward resolution values as-is. |
||||
|
$promise->resolve($value); |
||||
|
} else { |
||||
|
// Forward rejections down the chain. |
||||
|
$promise->reject($value); |
||||
|
} |
||||
|
} catch (\Throwable $reason) { |
||||
|
$promise->reject($reason); |
||||
|
} catch (\Exception $reason) { |
||||
|
$promise->reject($reason); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function waitIfPending() |
||||
|
{ |
||||
|
if ($this->state !== self::PENDING) { |
||||
|
return; |
||||
|
} elseif ($this->waitFn) { |
||||
|
$this->invokeWaitFn(); |
||||
|
} elseif ($this->waitList) { |
||||
|
$this->invokeWaitList(); |
||||
|
} else { |
||||
|
// If there's not wait function, then reject the promise. |
||||
|
$this->reject('Cannot wait on a promise that has ' |
||||
|
. 'no internal wait function. You must provide a wait ' |
||||
|
. 'function when constructing the promise to be able to ' |
||||
|
. 'wait on a promise.'); |
||||
|
} |
||||
|
|
||||
|
queue()->run(); |
||||
|
|
||||
|
if ($this->state === self::PENDING) { |
||||
|
$this->reject('Invoking the wait callback did not resolve the promise'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function invokeWaitFn() |
||||
|
{ |
||||
|
try { |
||||
|
$wfn = $this->waitFn; |
||||
|
$this->waitFn = null; |
||||
|
$wfn(true); |
||||
|
} catch (\Exception $reason) { |
||||
|
if ($this->state === self::PENDING) { |
||||
|
// The promise has not been resolved yet, so reject the promise |
||||
|
// with the exception. |
||||
|
$this->reject($reason); |
||||
|
} else { |
||||
|
// The promise was already resolved, so there's a problem in |
||||
|
// the application. |
||||
|
throw $reason; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private function invokeWaitList() |
||||
|
{ |
||||
|
$waitList = $this->waitList; |
||||
|
$this->waitList = null; |
||||
|
|
||||
|
foreach ($waitList as $result) { |
||||
|
while (true) { |
||||
|
$result->waitIfPending(); |
||||
|
|
||||
|
if ($result->result instanceof Promise) { |
||||
|
$result = $result->result; |
||||
|
} else { |
||||
|
if ($result->result instanceof PromiseInterface) { |
||||
|
$result->result->wait(false); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Promise; |
||||
|
|
||||
|
/** |
||||
|
* A promise represents the eventual result of an asynchronous operation. |
||||
|
* |
||||
|
* The primary way of interacting with a promise is through its then method, |
||||
|
* which registers callbacks to receive either a promise’s eventual value or |
||||
|
* the reason why the promise cannot be fulfilled. |
||||
|
* |
||||
|
* @link https://promisesaplus.com/ |
||||
|
*/ |
||||
|
interface PromiseInterface |
||||
|
{ |
||||
|
const PENDING = 'pending'; |
||||
|
const FULFILLED = 'fulfilled'; |
||||
|
const REJECTED = 'rejected'; |
||||
|
|
||||
|
/** |
||||
|
* Appends fulfillment and rejection handlers to the promise, and returns |
||||
|
* a new promise resolving to the return value of the called handler. |
||||
|
* |
||||
|
* @param callable $onFulfilled Invoked when the promise fulfills. |
||||
|
* @param callable $onRejected Invoked when the promise is rejected. |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
public function then( |
||||
|
callable $onFulfilled = null, |
||||
|
callable $onRejected = null |
||||
|
); |
||||
|
|
||||
|
/** |
||||
|
* Appends a rejection handler callback to the promise, and returns a new |
||||
|
* promise resolving to the return value of the callback if it is called, |
||||
|
* or to its original fulfillment value if the promise is instead |
||||
|
* fulfilled. |
||||
|
* |
||||
|
* @param callable $onRejected Invoked when the promise is rejected. |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
public function otherwise(callable $onRejected); |
||||
|
|
||||
|
/** |
||||
|
* Get the state of the promise ("pending", "rejected", or "fulfilled"). |
||||
|
* |
||||
|
* The three states can be checked against the constants defined on |
||||
|
* PromiseInterface: PENDING, FULFILLED, and REJECTED. |
||||
|
* |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getState(); |
||||
|
|
||||
|
/** |
||||
|
* Resolve the promise with the given value. |
||||
|
* |
||||
|
* @param mixed $value |
||||
|
* @throws \RuntimeException if the promise is already resolved. |
||||
|
*/ |
||||
|
public function resolve($value); |
||||
|
|
||||
|
/** |
||||
|
* Reject the promise with the given reason. |
||||
|
* |
||||
|
* @param mixed $reason |
||||
|
* @throws \RuntimeException if the promise is already resolved. |
||||
|
*/ |
||||
|
public function reject($reason); |
||||
|
|
||||
|
/** |
||||
|
* Cancels the promise if possible. |
||||
|
* |
||||
|
* @link https://github.com/promises-aplus/cancellation-spec/issues/7 |
||||
|
*/ |
||||
|
public function cancel(); |
||||
|
|
||||
|
/** |
||||
|
* Waits until the promise completes if possible. |
||||
|
* |
||||
|
* Pass $unwrap as true to unwrap the result of the promise, either |
||||
|
* returning the resolved value or throwing the rejected exception. |
||||
|
* |
||||
|
* If the promise cannot be waited on, then the promise will be rejected. |
||||
|
* |
||||
|
* @param bool $unwrap |
||||
|
* |
||||
|
* @return mixed |
||||
|
* @throws \LogicException if the promise has no wait function or if the |
||||
|
* promise does not settle after waiting. |
||||
|
*/ |
||||
|
public function wait($unwrap = true); |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Promise; |
||||
|
|
||||
|
/** |
||||
|
* Interface used with classes that return a promise. |
||||
|
*/ |
||||
|
interface PromisorInterface |
||||
|
{ |
||||
|
/** |
||||
|
* Returns a promise. |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
public function promise(); |
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Promise; |
||||
|
|
||||
|
/** |
||||
|
* A promise that has been rejected. |
||||
|
* |
||||
|
* Thenning off of this promise will invoke the onRejected callback |
||||
|
* immediately and ignore other callbacks. |
||||
|
*/ |
||||
|
class RejectedPromise implements PromiseInterface |
||||
|
{ |
||||
|
private $reason; |
||||
|
|
||||
|
public function __construct($reason) |
||||
|
{ |
||||
|
if (method_exists($reason, 'then')) { |
||||
|
throw new \InvalidArgumentException( |
||||
|
'You cannot create a RejectedPromise with a promise.'); |
||||
|
} |
||||
|
|
||||
|
$this->reason = $reason; |
||||
|
} |
||||
|
|
||||
|
public function then( |
||||
|
callable $onFulfilled = null, |
||||
|
callable $onRejected = null |
||||
|
) { |
||||
|
// If there's no onRejected callback then just return self. |
||||
|
if (!$onRejected) { |
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
$queue = queue(); |
||||
|
$reason = $this->reason; |
||||
|
$p = new Promise([$queue, 'run']); |
||||
|
$queue->add(static function () use ($p, $reason, $onRejected) { |
||||
|
if ($p->getState() === self::PENDING) { |
||||
|
try { |
||||
|
// Return a resolved promise if onRejected does not throw. |
||||
|
$p->resolve($onRejected($reason)); |
||||
|
} catch (\Throwable $e) { |
||||
|
// onRejected threw, so return a rejected promise. |
||||
|
$p->reject($e); |
||||
|
} catch (\Exception $e) { |
||||
|
// onRejected threw, so return a rejected promise. |
||||
|
$p->reject($e); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return $p; |
||||
|
} |
||||
|
|
||||
|
public function otherwise(callable $onRejected) |
||||
|
{ |
||||
|
return $this->then(null, $onRejected); |
||||
|
} |
||||
|
|
||||
|
public function wait($unwrap = true, $defaultDelivery = null) |
||||
|
{ |
||||
|
if ($unwrap) { |
||||
|
throw exception_for($this->reason); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function getState() |
||||
|
{ |
||||
|
return self::REJECTED; |
||||
|
} |
||||
|
|
||||
|
public function resolve($value) |
||||
|
{ |
||||
|
throw new \LogicException("Cannot resolve a rejected promise"); |
||||
|
} |
||||
|
|
||||
|
public function reject($reason) |
||||
|
{ |
||||
|
if ($reason !== $this->reason) { |
||||
|
throw new \LogicException("Cannot reject a rejected promise"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function cancel() |
||||
|
{ |
||||
|
// pass |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Promise; |
||||
|
|
||||
|
/** |
||||
|
* A special exception that is thrown when waiting on a rejected promise. |
||||
|
* |
||||
|
* The reason value is available via the getReason() method. |
||||
|
*/ |
||||
|
class RejectionException extends \RuntimeException |
||||
|
{ |
||||
|
/** @var mixed Rejection reason. */ |
||||
|
private $reason; |
||||
|
|
||||
|
/** |
||||
|
* @param mixed $reason Rejection reason. |
||||
|
* @param string $description Optional description |
||||
|
*/ |
||||
|
public function __construct($reason, $description = null) |
||||
|
{ |
||||
|
$this->reason = $reason; |
||||
|
|
||||
|
$message = 'The promise was rejected'; |
||||
|
|
||||
|
if ($description) { |
||||
|
$message .= ' with reason: ' . $description; |
||||
|
} elseif (is_string($reason) |
||||
|
|| (is_object($reason) && method_exists($reason, '__toString')) |
||||
|
) { |
||||
|
$message .= ' with reason: ' . $this->reason; |
||||
|
} elseif ($reason instanceof \JsonSerializable) { |
||||
|
$message .= ' with reason: ' |
||||
|
. json_encode($this->reason, JSON_PRETTY_PRINT); |
||||
|
} |
||||
|
|
||||
|
parent::__construct($message); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the rejection reason. |
||||
|
* |
||||
|
* @return mixed |
||||
|
*/ |
||||
|
public function getReason() |
||||
|
{ |
||||
|
return $this->reason; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Promise; |
||||
|
|
||||
|
/** |
||||
|
* A task queue that executes tasks in a FIFO order. |
||||
|
* |
||||
|
* This task queue class is used to settle promises asynchronously and |
||||
|
* maintains a constant stack size. You can use the task queue asynchronously |
||||
|
* by calling the `run()` function of the global task queue in an event loop. |
||||
|
* |
||||
|
* GuzzleHttp\Promise\queue()->run(); |
||||
|
*/ |
||||
|
class TaskQueue implements TaskQueueInterface |
||||
|
{ |
||||
|
private $enableShutdown = true; |
||||
|
private $queue = []; |
||||
|
|
||||
|
public function __construct($withShutdown = true) |
||||
|
{ |
||||
|
if ($withShutdown) { |
||||
|
register_shutdown_function(function () { |
||||
|
if ($this->enableShutdown) { |
||||
|
// Only run the tasks if an E_ERROR didn't occur. |
||||
|
$err = error_get_last(); |
||||
|
if (!$err || ($err['type'] ^ E_ERROR)) { |
||||
|
$this->run(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public function isEmpty() |
||||
|
{ |
||||
|
return !$this->queue; |
||||
|
} |
||||
|
|
||||
|
public function add(callable $task) |
||||
|
{ |
||||
|
$this->queue[] = $task; |
||||
|
} |
||||
|
|
||||
|
public function run() |
||||
|
{ |
||||
|
/** @var callable $task */ |
||||
|
while ($task = array_shift($this->queue)) { |
||||
|
$task(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* The task queue will be run and exhausted by default when the process |
||||
|
* exits IFF the exit is not the result of a PHP E_ERROR error. |
||||
|
* |
||||
|
* You can disable running the automatic shutdown of the queue by calling |
||||
|
* this function. If you disable the task queue shutdown process, then you |
||||
|
* MUST either run the task queue (as a result of running your event loop |
||||
|
* or manually using the run() method) or wait on each outstanding promise. |
||||
|
* |
||||
|
* Note: This shutdown will occur before any destructors are triggered. |
||||
|
*/ |
||||
|
public function disableShutdown() |
||||
|
{ |
||||
|
$this->enableShutdown = false; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Promise; |
||||
|
|
||||
|
interface TaskQueueInterface |
||||
|
{ |
||||
|
/** |
||||
|
* Returns true if the queue is empty. |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function isEmpty(); |
||||
|
|
||||
|
/** |
||||
|
* Adds a task to the queue that will be executed the next time run is |
||||
|
* called. |
||||
|
* |
||||
|
* @param callable $task |
||||
|
*/ |
||||
|
public function add(callable $task); |
||||
|
|
||||
|
/** |
||||
|
* Execute all of the pending task in the queue. |
||||
|
*/ |
||||
|
public function run(); |
||||
|
} |
||||
@ -0,0 +1,457 @@ |
|||||
|
<?php |
||||
|
namespace GuzzleHttp\Promise; |
||||
|
|
||||
|
/** |
||||
|
* Get the global task queue used for promise resolution. |
||||
|
* |
||||
|
* This task queue MUST be run in an event loop in order for promises to be |
||||
|
* settled asynchronously. It will be automatically run when synchronously |
||||
|
* waiting on a promise. |
||||
|
* |
||||
|
* <code> |
||||
|
* while ($eventLoop->isRunning()) { |
||||
|
* GuzzleHttp\Promise\queue()->run(); |
||||
|
* } |
||||
|
* </code> |
||||
|
* |
||||
|
* @param TaskQueueInterface $assign Optionally specify a new queue instance. |
||||
|
* |
||||
|
* @return TaskQueueInterface |
||||
|
*/ |
||||
|
function queue(TaskQueueInterface $assign = null) |
||||
|
{ |
||||
|
static $queue; |
||||
|
|
||||
|
if ($assign) { |
||||
|
$queue = $assign; |
||||
|
} elseif (!$queue) { |
||||
|
$queue = new TaskQueue(); |
||||
|
} |
||||
|
|
||||
|
return $queue; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Adds a function to run in the task queue when it is next `run()` and returns |
||||
|
* a promise that is fulfilled or rejected with the result. |
||||
|
* |
||||
|
* @param callable $task Task function to run. |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
function task(callable $task) |
||||
|
{ |
||||
|
$queue = queue(); |
||||
|
$promise = new Promise([$queue, 'run']); |
||||
|
$queue->add(function () use ($task, $promise) { |
||||
|
try { |
||||
|
$promise->resolve($task()); |
||||
|
} catch (\Throwable $e) { |
||||
|
$promise->reject($e); |
||||
|
} catch (\Exception $e) { |
||||
|
$promise->reject($e); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return $promise; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Creates a promise for a value if the value is not a promise. |
||||
|
* |
||||
|
* @param mixed $value Promise or value. |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
function promise_for($value) |
||||
|
{ |
||||
|
if ($value instanceof PromiseInterface) { |
||||
|
return $value; |
||||
|
} |
||||
|
|
||||
|
// Return a Guzzle promise that shadows the given promise. |
||||
|
if (method_exists($value, 'then')) { |
||||
|
$wfn = method_exists($value, 'wait') ? [$value, 'wait'] : null; |
||||
|
$cfn = method_exists($value, 'cancel') ? [$value, 'cancel'] : null; |
||||
|
$promise = new Promise($wfn, $cfn); |
||||
|
$value->then([$promise, 'resolve'], [$promise, 'reject']); |
||||
|
return $promise; |
||||
|
} |
||||
|
|
||||
|
return new FulfilledPromise($value); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Creates a rejected promise for a reason if the reason is not a promise. If |
||||
|
* the provided reason is a promise, then it is returned as-is. |
||||
|
* |
||||
|
* @param mixed $reason Promise or reason. |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
function rejection_for($reason) |
||||
|
{ |
||||
|
if ($reason instanceof PromiseInterface) { |
||||
|
return $reason; |
||||
|
} |
||||
|
|
||||
|
return new RejectedPromise($reason); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Create an exception for a rejected promise value. |
||||
|
* |
||||
|
* @param mixed $reason |
||||
|
* |
||||
|
* @return \Exception|\Throwable |
||||
|
*/ |
||||
|
function exception_for($reason) |
||||
|
{ |
||||
|
return $reason instanceof \Exception || $reason instanceof \Throwable |
||||
|
? $reason |
||||
|
: new RejectionException($reason); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns an iterator for the given value. |
||||
|
* |
||||
|
* @param mixed $value |
||||
|
* |
||||
|
* @return \Iterator |
||||
|
*/ |
||||
|
function iter_for($value) |
||||
|
{ |
||||
|
if ($value instanceof \Iterator) { |
||||
|
return $value; |
||||
|
} elseif (is_array($value)) { |
||||
|
return new \ArrayIterator($value); |
||||
|
} else { |
||||
|
return new \ArrayIterator([$value]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Synchronously waits on a promise to resolve and returns an inspection state |
||||
|
* array. |
||||
|
* |
||||
|
* Returns a state associative array containing a "state" key mapping to a |
||||
|
* valid promise state. If the state of the promise is "fulfilled", the array |
||||
|
* will contain a "value" key mapping to the fulfilled value of the promise. If |
||||
|
* the promise is rejected, the array will contain a "reason" key mapping to |
||||
|
* the rejection reason of the promise. |
||||
|
* |
||||
|
* @param PromiseInterface $promise Promise or value. |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
function inspect(PromiseInterface $promise) |
||||
|
{ |
||||
|
try { |
||||
|
return [ |
||||
|
'state' => PromiseInterface::FULFILLED, |
||||
|
'value' => $promise->wait() |
||||
|
]; |
||||
|
} catch (RejectionException $e) { |
||||
|
return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()]; |
||||
|
} catch (\Throwable $e) { |
||||
|
return ['state' => PromiseInterface::REJECTED, 'reason' => $e]; |
||||
|
} catch (\Exception $e) { |
||||
|
return ['state' => PromiseInterface::REJECTED, 'reason' => $e]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Waits on all of the provided promises, but does not unwrap rejected promises |
||||
|
* as thrown exception. |
||||
|
* |
||||
|
* Returns an array of inspection state arrays. |
||||
|
* |
||||
|
* @param PromiseInterface[] $promises Traversable of promises to wait upon. |
||||
|
* |
||||
|
* @return array |
||||
|
* @see GuzzleHttp\Promise\inspect for the inspection state array format. |
||||
|
*/ |
||||
|
function inspect_all($promises) |
||||
|
{ |
||||
|
$results = []; |
||||
|
foreach ($promises as $key => $promise) { |
||||
|
$results[$key] = inspect($promise); |
||||
|
} |
||||
|
|
||||
|
return $results; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Waits on all of the provided promises and returns the fulfilled values. |
||||
|
* |
||||
|
* Returns an array that contains the value of each promise (in the same order |
||||
|
* the promises were provided). An exception is thrown if any of the promises |
||||
|
* are rejected. |
||||
|
* |
||||
|
* @param mixed $promises Iterable of PromiseInterface objects to wait on. |
||||
|
* |
||||
|
* @return array |
||||
|
* @throws \Exception on error |
||||
|
* @throws \Throwable on error in PHP >=7 |
||||
|
*/ |
||||
|
function unwrap($promises) |
||||
|
{ |
||||
|
$results = []; |
||||
|
foreach ($promises as $key => $promise) { |
||||
|
$results[$key] = $promise->wait(); |
||||
|
} |
||||
|
|
||||
|
return $results; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Given an array of promises, return a promise that is fulfilled when all the |
||||
|
* items in the array are fulfilled. |
||||
|
* |
||||
|
* The promise's fulfillment value is an array with fulfillment values at |
||||
|
* respective positions to the original array. If any promise in the array |
||||
|
* rejects, the returned promise is rejected with the rejection reason. |
||||
|
* |
||||
|
* @param mixed $promises Promises or values. |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
function all($promises) |
||||
|
{ |
||||
|
$results = []; |
||||
|
return each( |
||||
|
$promises, |
||||
|
function ($value, $idx) use (&$results) { |
||||
|
$results[$idx] = $value; |
||||
|
}, |
||||
|
function ($reason, $idx, Promise $aggregate) { |
||||
|
$aggregate->reject($reason); |
||||
|
} |
||||
|
)->then(function () use (&$results) { |
||||
|
ksort($results); |
||||
|
return $results; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Initiate a competitive race between multiple promises or values (values will |
||||
|
* become immediately fulfilled promises). |
||||
|
* |
||||
|
* When count amount of promises have been fulfilled, the returned promise is |
||||
|
* fulfilled with an array that contains the fulfillment values of the winners |
||||
|
* in order of resolution. |
||||
|
* |
||||
|
* This prommise is rejected with a {@see GuzzleHttp\Promise\AggregateException} |
||||
|
* if the number of fulfilled promises is less than the desired $count. |
||||
|
* |
||||
|
* @param int $count Total number of promises. |
||||
|
* @param mixed $promises Promises or values. |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
function some($count, $promises) |
||||
|
{ |
||||
|
$results = []; |
||||
|
$rejections = []; |
||||
|
|
||||
|
return each( |
||||
|
$promises, |
||||
|
function ($value, $idx, PromiseInterface $p) use (&$results, $count) { |
||||
|
if ($p->getState() !== PromiseInterface::PENDING) { |
||||
|
return; |
||||
|
} |
||||
|
$results[$idx] = $value; |
||||
|
if (count($results) >= $count) { |
||||
|
$p->resolve(null); |
||||
|
} |
||||
|
}, |
||||
|
function ($reason) use (&$rejections) { |
||||
|
$rejections[] = $reason; |
||||
|
} |
||||
|
)->then( |
||||
|
function () use (&$results, &$rejections, $count) { |
||||
|
if (count($results) !== $count) { |
||||
|
throw new AggregateException( |
||||
|
'Not enough promises to fulfill count', |
||||
|
$rejections |
||||
|
); |
||||
|
} |
||||
|
ksort($results); |
||||
|
return array_values($results); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Like some(), with 1 as count. However, if the promise fulfills, the |
||||
|
* fulfillment value is not an array of 1 but the value directly. |
||||
|
* |
||||
|
* @param mixed $promises Promises or values. |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
function any($promises) |
||||
|
{ |
||||
|
return some(1, $promises)->then(function ($values) { return $values[0]; }); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns a promise that is fulfilled when all of the provided promises have |
||||
|
* been fulfilled or rejected. |
||||
|
* |
||||
|
* The returned promise is fulfilled with an array of inspection state arrays. |
||||
|
* |
||||
|
* @param mixed $promises Promises or values. |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
* @see GuzzleHttp\Promise\inspect for the inspection state array format. |
||||
|
*/ |
||||
|
function settle($promises) |
||||
|
{ |
||||
|
$results = []; |
||||
|
|
||||
|
return each( |
||||
|
$promises, |
||||
|
function ($value, $idx) use (&$results) { |
||||
|
$results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value]; |
||||
|
}, |
||||
|
function ($reason, $idx) use (&$results) { |
||||
|
$results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason]; |
||||
|
} |
||||
|
)->then(function () use (&$results) { |
||||
|
ksort($results); |
||||
|
return $results; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Given an iterator that yields promises or values, returns a promise that is |
||||
|
* fulfilled with a null value when the iterator has been consumed or the |
||||
|
* aggregate promise has been fulfilled or rejected. |
||||
|
* |
||||
|
* $onFulfilled is a function that accepts the fulfilled value, iterator |
||||
|
* index, and the aggregate promise. The callback can invoke any necessary side |
||||
|
* effects and choose to resolve or reject the aggregate promise if needed. |
||||
|
* |
||||
|
* $onRejected is a function that accepts the rejection reason, iterator |
||||
|
* index, and the aggregate promise. The callback can invoke any necessary side |
||||
|
* effects and choose to resolve or reject the aggregate promise if needed. |
||||
|
* |
||||
|
* @param mixed $iterable Iterator or array to iterate over. |
||||
|
* @param callable $onFulfilled |
||||
|
* @param callable $onRejected |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
function each( |
||||
|
$iterable, |
||||
|
callable $onFulfilled = null, |
||||
|
callable $onRejected = null |
||||
|
) { |
||||
|
return (new EachPromise($iterable, [ |
||||
|
'fulfilled' => $onFulfilled, |
||||
|
'rejected' => $onRejected |
||||
|
]))->promise(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Like each, but only allows a certain number of outstanding promises at any |
||||
|
* given time. |
||||
|
* |
||||
|
* $concurrency may be an integer or a function that accepts the number of |
||||
|
* pending promises and returns a numeric concurrency limit value to allow for |
||||
|
* dynamic a concurrency size. |
||||
|
* |
||||
|
* @param mixed $iterable |
||||
|
* @param int|callable $concurrency |
||||
|
* @param callable $onFulfilled |
||||
|
* @param callable $onRejected |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
function each_limit( |
||||
|
$iterable, |
||||
|
$concurrency, |
||||
|
callable $onFulfilled = null, |
||||
|
callable $onRejected = null |
||||
|
) { |
||||
|
return (new EachPromise($iterable, [ |
||||
|
'fulfilled' => $onFulfilled, |
||||
|
'rejected' => $onRejected, |
||||
|
'concurrency' => $concurrency |
||||
|
]))->promise(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Like each_limit, but ensures that no promise in the given $iterable argument |
||||
|
* is rejected. If any promise is rejected, then the aggregate promise is |
||||
|
* rejected with the encountered rejection. |
||||
|
* |
||||
|
* @param mixed $iterable |
||||
|
* @param int|callable $concurrency |
||||
|
* @param callable $onFulfilled |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
function each_limit_all( |
||||
|
$iterable, |
||||
|
$concurrency, |
||||
|
callable $onFulfilled = null |
||||
|
) { |
||||
|
return each_limit( |
||||
|
$iterable, |
||||
|
$concurrency, |
||||
|
$onFulfilled, |
||||
|
function ($reason, $idx, PromiseInterface $aggregate) { |
||||
|
$aggregate->reject($reason); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns true if a promise is fulfilled. |
||||
|
* |
||||
|
* @param PromiseInterface $promise |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
function is_fulfilled(PromiseInterface $promise) |
||||
|
{ |
||||
|
return $promise->getState() === PromiseInterface::FULFILLED; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns true if a promise is rejected. |
||||
|
* |
||||
|
* @param PromiseInterface $promise |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
function is_rejected(PromiseInterface $promise) |
||||
|
{ |
||||
|
return $promise->getState() === PromiseInterface::REJECTED; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns true if a promise is fulfilled or rejected. |
||||
|
* |
||||
|
* @param PromiseInterface $promise |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
function is_settled(PromiseInterface $promise) |
||||
|
{ |
||||
|
return $promise->getState() !== PromiseInterface::PENDING; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @see Coroutine |
||||
|
* |
||||
|
* @param callable $generatorFn |
||||
|
* |
||||
|
* @return PromiseInterface |
||||
|
*/ |
||||
|
function coroutine(callable $generatorFn) |
||||
|
{ |
||||
|
return new Coroutine($generatorFn); |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
<?php |
||||
|
|
||||
|
// Don't redefine the functions if included multiple times. |
||||
|
if (!function_exists('GuzzleHttp\Promise\promise_for')) { |
||||
|
require __DIR__ . '/functions.php'; |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue