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