Update to Drupal 8.0.0 beta 14. For more information, see https://drupal.org/node/2544542

This commit is contained in:
Pantheon Automation 2015-08-27 12:03:05 -07:00 committed by Greg Anderson
parent 3b2511d96d
commit 81ccda77eb
2155 changed files with 54307 additions and 46870 deletions

View file

@ -2,6 +2,10 @@ language: php
php: [5.3, 5.4, 5.5, 5.6, hhvm]
before_install:
# Force using Goutte 2 on HHVM for now because Guzzle 6 is broken there
- if [ "hhvm" = "$TRAVIS_PHP_VERSION" ]; then composer require fabpot/goutte '~2' --no-update; fi
before_script:
- export WEB_FIXTURES_HOST=http://localhost

25
core/vendor/behat/mink-goutte-driver/README.md vendored Executable file → Normal file
View file

@ -4,9 +4,9 @@ Mink Goutte Driver
[![Latest Stable Version](https://poser.pugx.org/behat/mink-goutte-driver/v/stable.svg)](https://packagist.org/packages/behat/mink-goutte-driver)
[![Latest Unstable Version](https://poser.pugx.org/behat/mink-goutte-driver/v/unstable.svg)](https://packagist.org/packages/behat/mink-goutte-driver)
[![Total Downloads](https://poser.pugx.org/behat/mink-goutte-driver/downloads.svg)](https://packagist.org/packages/behat/mink-goutte-driver)
[![Build Status](https://travis-ci.org/Behat/MinkGoutteDriver.svg?branch=master)](https://travis-ci.org/Behat/MinkGoutteDriver)
[![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/Behat/MinkGoutteDriver/badges/quality-score.png?s=ca141bb2cad18e74cf3d3b132b1a6aa0f3f004a5)](https://scrutinizer-ci.com/g/Behat/MinkGoutteDriver/)
[![Code Coverage](https://scrutinizer-ci.com/g/Behat/MinkGoutteDriver/badges/coverage.png?s=ca2d17a948660bfaeb4a95bf1a709644305c54f3)](https://scrutinizer-ci.com/g/Behat/MinkGoutteDriver/)
[![Build Status](https://travis-ci.org/minkphp/MinkGoutteDriver.svg?branch=master)](https://travis-ci.org/minkphp/MinkGoutteDriver)
[![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/minkphp/MinkGoutteDriver/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/minkphp/MinkGoutteDriver/)
[![Code Coverage](https://scrutinizer-ci.com/g/minkphp/MinkGoutteDriver/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/minkphp/MinkGoutteDriver/)
[![License](https://poser.pugx.org/behat/mink-goutte-driver/license.svg)](https://packagist.org/packages/behat/mink-goutte-driver)
Usage Example
@ -15,23 +15,28 @@ Usage Example
``` php
<?php
require "vendor/autoload.php";
use Behat\Mink\Mink,
Behat\Mink\Session,
Behat\Mink\Driver\GoutteDriver,
Behat\Mink\Driver\Goutte\Client as GoutteClient;
$startUrl = 'http://example.com';
$mink = new Mink(array(
'goutte' => new Session(new GoutteDriver(new GoutteClient($startUrl))),
'goutte' => new Session(new GoutteDriver(new GoutteClient())),
));
$mink->getSession('goutte')->getPage()->findLink('Chat')->click();
$session = $mink->getSession('goutte');
$session->visit("http://php.net/");
$session->getPage()->clickLink('Downloads');
echo $session->getCurrentUrl() . PHP_EOL;
```
Installation
------------
Add a file composer.json with content:
``` json
{
"require": {
@ -41,6 +46,8 @@ Installation
}
```
(or merge the above into your project's existing composer.json file)
``` bash
$> curl -sS https://getcomposer.org/installer | php
$> php composer.phar install
@ -49,5 +56,5 @@ $> php composer.phar install
Maintainers
-----------
* Konstantin Kudryashov [everzet](http://github.com/everzet)
* Other [awesome developers](https://github.com/Behat/MinkGoutteDriver/graphs/contributors)
* Christophe Coevoet [stof](https://github.com/stof)
* Other [awesome developers](https://github.com/minkphp/MinkGoutteDriver/graphs/contributors)

View file

@ -18,12 +18,12 @@
"php": ">=5.3.1",
"behat/mink": "~1.6@dev",
"behat/mink-browserkit-driver": "~1.2@dev",
"fabpot/goutte": "~1.0.4|~2.0"
"fabpot/goutte": "~1.0.4|~2.0|~3.1"
},
"autoload": {
"psr-0": {
"Behat\\Mink\\Driver": "src/"
"psr-4": {
"Behat\\Mink\\Driver\\": "src/"
}
},

View file

@ -17,7 +17,7 @@
<filter>
<whitelist>
<directory>./src/Behat/Mink/Driver</directory>
<directory>./src</directory>
</whitelist>
</filter>
</phpunit>

View file

@ -6,6 +6,8 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
$vendorDir . '/react/promise/src/functions_include.php',
$vendorDir . '/guzzlehttp/psr7/src/functions.php',
$vendorDir . '/guzzlehttp/promises/src/functions.php',
$vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
$baseDir . '/lib/Drupal.php',
);

View file

@ -12,7 +12,6 @@ return array(
'Stack' => array($vendorDir . '/stack/builder/src'),
'Psr\\Log\\' => array($vendorDir . '/psr/log'),
'Prophecy\\' => array($vendorDir . '/phpspec/prophecy/src'),
'Gliph' => array($vendorDir . '/sdboyer/gliph/src'),
'Egulias\\' => array($vendorDir . '/egulias/email-validator/src'),
'EasyRdf_' => array($vendorDir . '/easyrdf/easyrdf/lib'),
'Doctrine\\Instantiator\\' => array($vendorDir . '/doctrine/instantiator/src'),
@ -22,5 +21,5 @@ return array(
'Doctrine\\Common\\Cache\\' => array($vendorDir . '/doctrine/cache/lib'),
'Doctrine\\Common\\Annotations\\' => array($vendorDir . '/doctrine/annotations/lib'),
'Doctrine\\Common\\' => array($vendorDir . '/doctrine/common/lib'),
'Behat\\Mink\\Driver' => array($vendorDir . '/behat/mink-browserkit-driver/src', $vendorDir . '/behat/mink-goutte-driver/src'),
'Behat\\Mink\\Driver' => array($vendorDir . '/behat/mink-browserkit-driver/src'),
);

View file

@ -9,6 +9,7 @@ return array(
'Zend\\Stdlib\\' => array($vendorDir . '/zendframework/zend-stdlib'),
'Zend\\Feed\\' => array($vendorDir . '/zendframework/zend-feed'),
'Zend\\Escaper\\' => array($vendorDir . '/zendframework/zend-escaper'),
'Zend\\Diactoros\\' => array($vendorDir . '/zendframework/zend-diactoros/src'),
'Symfony\\Component\\Yaml\\' => array($vendorDir . '/symfony/yaml'),
'Symfony\\Component\\Validator\\' => array($vendorDir . '/symfony/validator'),
'Symfony\\Component\\Translation\\' => array($vendorDir . '/symfony/translation'),
@ -26,14 +27,16 @@ return array(
'Symfony\\Component\\ClassLoader\\' => array($vendorDir . '/symfony/class-loader'),
'Symfony\\Component\\BrowserKit\\' => array($vendorDir . '/symfony/browser-kit'),
'Symfony\\Cmf\\Component\\Routing\\' => array($vendorDir . '/symfony-cmf/routing'),
'React\\Promise\\' => array($vendorDir . '/react/promise/src'),
'Symfony\\Bridge\\PsrHttpMessage\\' => array($vendorDir . '/symfony/psr-http-message-bridge'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'),
'Masterminds\\' => array($vendorDir . '/masterminds/html5/src'),
'GuzzleHttp\\Stream\\' => array($vendorDir . '/guzzlehttp/streams/src'),
'GuzzleHttp\\Ring\\' => array($vendorDir . '/guzzlehttp/ringphp/src'),
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
'Goutte\\' => array($vendorDir . '/fabpot/goutte/Goutte'),
'Drupal\\Driver\\' => array($baseDir . '/../drivers/lib/Drupal/Driver'),
'Drupal\\Core\\' => array($baseDir . '/lib/Drupal/Core'),
'Drupal\\Component\\' => array($baseDir . '/lib/Drupal/Component'),
'Behat\\Mink\\Driver\\' => array($vendorDir . '/behat/mink-goutte-driver/src'),
'Behat\\Mink\\' => array($vendorDir . '/behat/mink/src'),
);

File diff suppressed because it is too large Load diff

19
core/vendor/doctrine/lexer/LICENSE vendored Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2006-2013 Doctrine Project
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.

View file

@ -15,5 +15,10 @@
},
"autoload": {
"psr-0": { "Doctrine\\Common\\Lexer\\": "lib/" }
},
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
}
}

View file

@ -22,35 +22,58 @@ namespace Doctrine\Common\Lexer;
/**
* Base class for writing simple lexers, i.e. for creating small DSLs.
*
* @since 2.0
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
* @since 2.0
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
abstract class AbstractLexer
{
/**
* @var array Array of scanned tokens
* Lexer original input string.
*
* @var string
*/
private $input;
/**
* Array of scanned tokens.
*
* Each token is an associative array containing three items:
* - 'value' : the string value of the token in the input string
* - 'type' : the type of the token (identifier, numeric, string, input
* parameter, none)
* - 'position' : the position of the token in the input string
*
* @var array
*/
private $tokens = array();
/**
* @var integer Current lexer position in input string
* Current lexer position in input string.
*
* @var integer
*/
private $position = 0;
/**
* @var integer Current peek of current lexer position
* Current peek of current lexer position.
*
* @var integer
*/
private $peek = 0;
/**
* @var array The next token in the input.
* The next token in the input.
*
* @var array
*/
public $lookahead;
/**
* @var array The last matched/seen token.
* The last matched/seen token.
*
* @var array
*/
public $token;
@ -61,16 +84,22 @@ abstract class AbstractLexer
* Any unprocessed tokens from any previous input are lost.
*
* @param string $input The input to be tokenized.
*
* @return void
*/
public function setInput($input)
{
$this->input = $input;
$this->tokens = array();
$this->reset();
$this->scan($input);
}
/**
* Resets the lexer.
*
* @return void
*/
public function reset()
{
@ -82,6 +111,8 @@ abstract class AbstractLexer
/**
* Resets the peek pointer to 0.
*
* @return void
*/
public function resetPeek()
{
@ -91,17 +122,32 @@ abstract class AbstractLexer
/**
* Resets the lexer position on the input to the given position.
*
* @param integer $position Position to place the lexical scanner
* @param integer $position Position to place the lexical scanner.
*
* @return void
*/
public function resetPosition($position = 0)
{
$this->position = $position;
}
/**
* Retrieve the original lexer's input until a given position.
*
* @param integer $position
*
* @return string
*/
public function getInputUntilPosition($position)
{
return substr($this->input, 0, $position);
}
/**
* Checks whether a given token matches the current lookahead.
*
* @param integer|string $token
*
* @return boolean
*/
public function isNextToken($token)
@ -110,9 +156,10 @@ abstract class AbstractLexer
}
/**
* Checks whether any of the given tokens matches the current lookahead
* Checks whether any of the given tokens matches the current lookahead.
*
* @param array $tokens
*
* @return boolean
*/
public function isNextTokenAny(array $tokens)
@ -123,13 +170,7 @@ abstract class AbstractLexer
/**
* Moves to the next token in the input string.
*
* A token is an associative array containing three items:
* - 'value' : the string value of the token in the input string
* - 'type' : the type of the token (identifier, numeric, string, input
* parameter, none)
* - 'position' : the position of the token in the input string
*
* @return array|null the next token; null if there is no more tokens left
* @return boolean
*/
public function moveNext()
{
@ -145,6 +186,8 @@ abstract class AbstractLexer
* Tells the lexer to skip input tokens until it sees a token with the given value.
*
* @param string $type The token type to skip until.
*
* @return void
*/
public function skipUntil($type)
{
@ -154,10 +197,11 @@ abstract class AbstractLexer
}
/**
* Checks if given value is identical to the given token
* Checks if given value is identical to the given token.
*
* @param mixed $value
* @param mixed $value
* @param integer $token
*
* @return boolean
*/
public function isA($value, $token)
@ -168,7 +212,7 @@ abstract class AbstractLexer
/**
* Moves the lookahead token forward.
*
* @return array | null The next token or NULL if there are no more tokens ahead.
* @return array|null The next token or NULL if there are no more tokens ahead.
*/
public function peek()
{
@ -194,15 +238,21 @@ abstract class AbstractLexer
/**
* Scans the input string for tokens.
*
* @param string $input a query string
* @param string $input A query string.
*
* @return void
*/
protected function scan($input)
{
static $regex;
if ( ! isset($regex)) {
$regex = '/(' . implode(')|(', $this->getCatchablePatterns()) . ')|'
. implode('|', $this->getNonCatchablePatterns()) . '/i';
$regex = sprintf(
'/(%s)|%s/%s',
implode(')|(', $this->getCatchablePatterns()),
implode('|', $this->getNonCatchablePatterns()),
$this->getModifiers()
);
}
$flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE;
@ -224,6 +274,7 @@ abstract class AbstractLexer
* Gets the literal for a given token.
*
* @param integer $token
*
* @return string
*/
public function getLiteral($token)
@ -241,6 +292,16 @@ abstract class AbstractLexer
return $token;
}
/**
* Regex modifiers
*
* @return string
*/
protected function getModifiers()
{
return 'i';
}
/**
* Lexical catchable patterns.
*
@ -259,6 +320,7 @@ abstract class AbstractLexer
* Retrieve token type. Also processes the token value if necessary.
*
* @param string $value
*
* @return integer
*/
abstract protected function getType(&$value);

View file

@ -0,0 +1,2 @@
report/
vendor/

View file

@ -1,3 +1,5 @@
sudo: false
language: php
php:
@ -5,11 +7,25 @@ php:
- 5.4
- 5.5
- 5.6
- 7.0
- hhvm
before_script:
- wget http://getcomposer.org/composer.phar
- php composer.phar install --dev --no-interaction
env:
global:
- deps=no
matrix:
fast_finish: true
include:
- php: 5.3
env: deps=low
- php: 5.6
env: deps=high
install:
- if [ "$deps" = "no" ]; then composer install; fi
- if [ "$deps" = "low" ]; then composer update --prefer-lowest; fi
- if [ "$deps" = "high" ]; then composer update; fi
script:
- mkdir -p build/logs

View file

@ -1,6 +1,9 @@
EmailValidator [![Build Status](https://travis-ci.org/egulias/EmailValidator.png?branch=master)](https://travis-ci.org/egulias/EmailValidator) [![Coverage Status](https://coveralls.io/repos/egulias/EmailValidator/badge.png?branch=master)](https://coveralls.io/r/egulias/EmailValidator?branch=master) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/22ba6692-9c02-42e5-a65d-1c5696bfffc6/small.png)](https://insight.sensiolabs.com/projects/22ba6692-9c02-42e5-a65d-1c5696bfffc6)[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/egulias/EmailValidator/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/egulias/EmailValidator/?branch=master)
#EmailValidator
[![Build Status](https://travis-ci.org/egulias/EmailValidator.png?branch=master)](https://travis-ci.org/egulias/EmailValidator) [![Coverage Status](https://coveralls.io/repos/egulias/EmailValidator/badge.png?branch=master)](https://coveralls.io/r/egulias/EmailValidator?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/egulias/EmailValidator/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/egulias/EmailValidator/?branch=master) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/22ba6692-9c02-42e5-a65d-1c5696bfffc6/small.png)](https://insight.sensiolabs.com/projects/22ba6692-9c02-42e5-a65d-1c5696bfffc6)
=============================
With the help of
![Powered by PhpStorm](https://www.jetbrains.com/phpstorm/documentation/docs/logo_phpstorm.png)
##Installation##
Run the command below to install via Composer

View file

@ -3,22 +3,23 @@
"description": "A library for validating emails",
"homepage": "https://github.com/egulias/EmailValidator",
"type": "Library",
"keywords": ["email", "validation", "validator"],
"keywords": ["email", "validation", "validator", "emailvalidation", "emailvalidator"],
"license": "MIT",
"authors": [
{"name": "Eduardo Gulias Davis"}
],
"extra": {
"branch-alias": {
"dev-master": "1.3.x-dev"
"dev-master": "2.0.x-dev"
}
},
"require": {
"php": ">= 5.3.3",
"doctrine/lexer": "~1.0"
"doctrine/lexer": "~1.0,>=1.0.1"
},
"require-dev" : {
"satooshi/php-coveralls": "dev-master"
"satooshi/php-coveralls": "dev-master",
"phpunit/phpunit": "~4.4"
},
"autoload": {
"psr-0": {

File diff suppressed because it is too large Load diff

View file

@ -75,8 +75,6 @@ class EmailLexer extends AbstractLexer
'\0' => self::C_NUL,
);
protected $invalidASCII = array(226 => 1,);
protected $hasInvalidTokens = false;
protected $previous;
@ -138,12 +136,12 @@ class EmailLexer extends AbstractLexer
protected function getCatchablePatterns()
{
return array(
'[a-zA-Z_]+[46]?',
'[a-zA-Z_]+[46]?', //ASCII and domain literal
'[^\x00-\x7F]', //UTF-8
'[0-9]+',
'\r\n',
'::',
'\s+',
'[\x10-\x1F]+',
'\s+?',
'.',
);
}
@ -155,7 +153,7 @@ class EmailLexer extends AbstractLexer
*/
protected function getNonCatchablePatterns()
{
return array('[\x7f-\xff]+');
return array('[\xA0-\xff]+');
}
/**
@ -167,16 +165,15 @@ class EmailLexer extends AbstractLexer
*/
protected function getType(&$value)
{
if ($this->isNullType($value)) {
return self::C_NUL;
}
if (isset($this->charValue[$value])) {
if ($this->isValid($value)) {
return $this->charValue[$value];
}
if ($this->isInvalid($value)) {
if ($this->isUTF8Invalid($value)) {
$this->hasInvalidTokens = true;
return self::INVALID;
}
@ -184,8 +181,18 @@ class EmailLexer extends AbstractLexer
return self::GENERIC;
}
protected function isValid($value)
{
if (isset($this->charValue[$value])) {
return true;
}
return false;
}
/**
* @param string $value
* @param $value
* @return bool
*/
protected function isNullType($value)
{
@ -197,18 +204,20 @@ class EmailLexer extends AbstractLexer
}
/**
* @param string $value
* @param $value
* @return bool
*/
protected function isInvalid($value)
protected function isUTF8Invalid($value)
{
if (preg_match('/[\x10-\x1F]+/', $value)) {
return true;
}
if (isset($this->invalidASCII[ord($value)])) {
if (preg_match('/\p{Cc}+/u', $value)) {
return true;
}
return false;
}
protected function getModifiers()
{
return 'iu';
}
}

View file

@ -29,7 +29,8 @@ class EmailParser
}
/**
* @param string $str
* @param $str
* @return array
*/
public function parse($str)
{
@ -39,15 +40,16 @@ class EmailParser
throw new \InvalidArgumentException('ERR_NOLOCALPART');
}
if ($this->lexer->hasInvalidTokens()) {
throw new \InvalidArgumentException('ERR_INVALID_ATEXT');
}
$this->localPartParser->parse($str);
$this->domainPartParser->parse($str);
$this->setParts($str);
if ($this->lexer->hasInvalidTokens()) {
throw new \InvalidArgumentException('ERR_INVALID_ATEXT');
}
return array('local' => $this->localPart, 'domain' => $this->domainPart);
}

View file

@ -104,6 +104,7 @@ class DomainPart extends Parser
{
$domain = '';
do {
$prev = $this->lexer->getPrevious();
if ($this->lexer->token['type'] === EmailLexer::S_SLASH) {
@ -217,9 +218,6 @@ class DomainPart extends Parser
return $addressLiteral;
}
/**
* @param string $addressLiteral
*/
protected function checkIPV4Tag($addressLiteral)
{
$matchesIP = array();
@ -245,6 +243,17 @@ class DomainPart extends Parser
protected function checkDomainPartExceptions($prev)
{
$invalidDomainTokens = array(
EmailLexer::S_DQUOTE => true,
EmailLexer::S_SEMICOLON => true,
EmailLexer::S_GREATERTHAN => true,
EmailLexer::S_LOWERTHAN => true,
);
if (isset($invalidDomainTokens[$this->lexer->token['type']])) {
throw new \InvalidArgumentException('ERR_EXPECTING_ATEXT');
}
if ($this->lexer->token['type'] === EmailLexer::S_COMMA) {
throw new \InvalidArgumentException('ERR_COMMA_IN_DOMAIN');
}

View file

@ -6,7 +6,6 @@ use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\EmailValidator;
use \InvalidArgumentException;
class LocalPart extends Parser
{
public function parse($localPart)

View file

@ -26,6 +26,73 @@ class EmailLexerTests extends \PHPUnit_Framework_TestCase
$this->assertEquals($token, $lexer->token['type']);
}
public function testLexerParsesMultipleSpaces()
{
$lexer = new EmailLexer();
$lexer->setInput(' ');
$lexer->moveNext();
$lexer->moveNext();
$this->assertEquals(EmailLexer::S_SP, $lexer->token['type']);
$lexer->moveNext();
$this->assertEquals(EmailLexer::S_SP, $lexer->token['type']);
}
/**
* @dataProvider invalidUTF8CharsProvider
*/
public function testLexerParsesInvalidUTF8($char)
{
$lexer = new EmailLexer();
$lexer->setInput($char);
$lexer->moveNext();
$lexer->moveNext();
$this->assertEquals(EmailLexer::INVALID, $lexer->token['type']);
}
public function invalidUTF8CharsProvider()
{
$chars = array();
for ($i = 0; $i < 0x100; ++$i) {
$c = $this->utf8Chr($i);
if (preg_match('/(?=\p{Cc})(?=[^\t\n\n\r])/u', $c) && !preg_match('/\x{0000}/u', $c)) {
$chars[] = array($c);
}
}
return $chars;
}
protected function utf8Chr($code_point)
{
if ($code_point < 0 || 0x10FFFF < $code_point || (0xD800 <= $code_point && $code_point <= 0xDFFF)) {
return '';
}
if ($code_point < 0x80) {
$hex[0] = $code_point;
$ret = chr($hex[0]);
} elseif ($code_point < 0x800) {
$hex[0] = 0x1C0 | $code_point >> 6;
$hex[1] = 0x80 | $code_point & 0x3F;
$ret = chr($hex[0]).chr($hex[1]);
} elseif ($code_point < 0x10000) {
$hex[0] = 0xE0 | $code_point >> 12;
$hex[1] = 0x80 | $code_point >> 6 & 0x3F;
$hex[2] = 0x80 | $code_point & 0x3F;
$ret = chr($hex[0]).chr($hex[1]).chr($hex[2]);
} else {
$hex[0] = 0xF0 | $code_point >> 18;
$hex[1] = 0x80 | $code_point >> 12 & 0x3F;
$hex[2] = 0x80 | $code_point >> 6 & 0x3F;
$hex[3] = 0x80 | $code_point & 0x3F;
$ret = chr($hex[0]).chr($hex[1]).chr($hex[2]).chr($hex[3]);
}
return $ret;
}
public function testLexerForTab()
{
$lexer = new EmailLexer();
@ -36,6 +103,17 @@ class EmailLexerTests extends \PHPUnit_Framework_TestCase
$this->assertEquals(EmailLexer::S_HTAB, $lexer->token['type']);
}
public function testLexerForUTF8()
{
$lexer = new EmailLexer();
$lexer->setInput("áÇ@bar.com");
$lexer->moveNext();
$lexer->moveNext();
$this->assertEquals(EmailLexer::GENERIC, $lexer->token['type']);
$lexer->moveNext();
$this->assertEquals(EmailLexer::GENERIC, $lexer->token['type']);
}
public function testLexerSearchToken()
{
$lexer = new EmailLexer();
@ -44,15 +122,6 @@ class EmailLexerTests extends \PHPUnit_Framework_TestCase
$this->assertTrue($lexer->find(EmailLexer::S_HTAB));
}
public function testLexerHasInvalidTokens()
{
$lexer = new EmailLexer();
$lexer->setInput(chr(226));
$lexer->moveNext();
$lexer->moveNext();
$this->assertTrue($lexer->hasInvalidTokens());
}
public function getTokens()
{
return array(
@ -85,7 +154,7 @@ class EmailLexerTests extends \PHPUnit_Framework_TestCase
array('}', EmailLexer::S_CLOSEQBRACKET),
array('', EmailLexer::S_EMPTY),
array(chr(31), EmailLexer::INVALID),
array(chr(226), EmailLexer::INVALID),
array(chr(226), EmailLexer::GENERIC),
array(chr(0), EmailLexer::C_NUL)
);
}

View file

@ -26,9 +26,18 @@ class EmailValidatorTest extends \PHPUnit_Framework_TestCase
$this->assertTrue($this->validator->isValid($email));
}
public function testInvalidUTF8Email()
{
$validator = new EmailValidator;
$email = "\x80\x81\x82@\x83\x84\x85.\x86\x87\x88";
$this->assertFalse($validator->isValid($email));
}
public function getValidEmails()
{
return array(
array('â@iana.org'),
array('fabien@symfony.com'),
array('example@example.co.uk'),
array('fabien_potencier@example.fr'),
@ -47,6 +56,13 @@ class EmailValidatorTest extends \PHPUnit_Framework_TestCase
array('"test\ test"@iana.org'),
array('""@iana.org'),
array('"\""@iana.org'),
array('müller@möller.de'),
array('test@email*'),
array('test@email!'),
array('test@email&'),
array('test@email^'),
array('test@email%'),
array('test@email$'),
);
}
@ -61,7 +77,9 @@ class EmailValidatorTest extends \PHPUnit_Framework_TestCase
public function getInvalidEmails()
{
return array(
array('test@example.com test'),
array('user name@example.com'),
array('user name@example.com'),
array('example.@example.co.uk'),
array('example@example@example.co.uk'),
array('(test_exampel@example.fr)'),
@ -98,6 +116,13 @@ class EmailValidatorTest extends \PHPUnit_Framework_TestCase
array('test@iana.org \r\n\r\n'),
array('test@iana.org \r\n\r\n '),
array('test@iana/icann.org'),
array('test@foo;bar.com'),
array('test;123@foobar.com'),
array('test@example..com'),
array('email.email@email."'),
array('test@email>'),
array('test@email<'),
array('test@email{'),
);
}
@ -163,13 +188,32 @@ class EmailValidatorTest extends \PHPUnit_Framework_TestCase
public function getInvalidEmailsWithWarnings()
{
return array(
array(array( EmailValidator::DEPREC_CFWS_NEAR_AT,), 'example @example.co.uk'),
array(array( EmailValidator::DEPREC_CFWS_NEAR_AT,), 'example@ example.co.uk'),
array(array( EmailValidator::CFWS_COMMENT,), 'example@example(examplecomment).co.uk'),
array(
array(
EmailValidator::DEPREC_CFWS_NEAR_AT,
EmailValidator::DNSWARN_NO_RECORD
),
'example @example.co.uk'
),
array(
array(
EmailValidator::DEPREC_CFWS_NEAR_AT,
EmailValidator::DNSWARN_NO_RECORD
),
'example@ example.co.uk'
),
array(
array(
EmailValidator::CFWS_COMMENT,
EmailValidator::DNSWARN_NO_RECORD
),
'example@example(examplecomment).co.uk'
),
array(
array(
EmailValidator::CFWS_COMMENT,
EmailValidator::DEPREC_CFWS_NEAR_AT,
EmailValidator::DNSWARN_NO_RECORD,
),
'example(examplecomment)@example.co.uk'
),
@ -177,6 +221,7 @@ class EmailValidatorTest extends \PHPUnit_Framework_TestCase
array(
EmailValidator::RFC5321_QUOTEDSTRING,
EmailValidator::CFWS_FWS,
EmailValidator::DNSWARN_NO_RECORD,
),
"\"\t\"@example.co.uk"
),
@ -184,6 +229,7 @@ class EmailValidatorTest extends \PHPUnit_Framework_TestCase
array(
EmailValidator::RFC5321_QUOTEDSTRING,
EmailValidator::CFWS_FWS,
EmailValidator::DNSWARN_NO_RECORD
),
"\"\r\"@example.co.uk"
),
@ -275,12 +321,14 @@ class EmailValidatorTest extends \PHPUnit_Framework_TestCase
array(
array(
EmailValidator::RFC5321_QUOTEDSTRING,
EmailValidator::DNSWARN_NO_RECORD
),
'"example"@example.co.uk'
),
array(
array(
EmailValidator::RFC5322_LOCAL_TOOLONG,
EmailValidator::DNSWARN_NO_RECORD
),
'too_long_localpart_too_long_localpart_too_long_localpart_too_long_localpart@example.co.uk'
),

View file

@ -1,9 +1,9 @@
language: php
php:
- 7.0
- 5.6
- 5.5
- 5.4
- hhvm
install:
@ -15,3 +15,4 @@ script:
matrix:
allow_failures:
- php: hhvm
- php: 7.0

View file

@ -13,10 +13,9 @@ namespace Goutte;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\ClientInterface as GuzzleClientInterface;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\Response as GuzzleResponse;
use GuzzleHttp\Post\PostFile;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\BrowserKit\Client as BaseClient;
use Symfony\Component\BrowserKit\Response;
@ -25,6 +24,7 @@ use Symfony\Component\BrowserKit\Response;
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
* @author Michael Dowling <michael@guzzlephp.org>
* @author Charles Sarrazin <charles@sarraz.in>
*/
class Client extends BaseClient
{
@ -90,45 +90,46 @@ class Client extends BaseClient
}
}
$body = null;
if (!in_array($request->getMethod(), array('GET', 'HEAD'))) {
if (null !== $request->getContent()) {
$body = $request->getContent();
} else {
$body = $request->getParameters();
}
}
$this->getClient()->setDefaultOption('auth', $this->auth);
$cookies = CookieJar::fromArray(
$this->getCookieJar()->allRawValues($request->getUri()),
$request->getServer()['HTTP_HOST']
);
$requestOptions = array(
'body' => $body,
'cookies' => $this->getCookieJar()->allRawValues($request->getUri()),
'cookies' => $cookies,
'allow_redirects' => false,
'timeout' => 30,
'auth' => $this->auth,
);
if (!in_array($request->getMethod(), array('GET', 'HEAD'))) {
if (null !== $content = $request->getContent()) {
$requestOptions['body'] = $content;
} else {
if ($files = $request->getFiles()) {
$requestOptions['multipart'] = [];
$this->addPostFields($request->getParameters(), $requestOptions['multipart']);
$this->addPostFiles($files, $requestOptions['multipart']);
} else {
$requestOptions['form_params'] = $request->getParameters();
}
}
}
if (!empty($headers)) {
$requestOptions['headers'] = $headers;
}
$guzzleRequest = $this->getClient()->createRequest(
$request->getMethod(),
$request->getUri(),
$requestOptions
);
$method = $request->getMethod();
$uri = $request->getUri();
foreach ($this->headers as $name => $value) {
$guzzleRequest->setHeader($name, $value);
}
if ('POST' == $request->getMethod() || 'PUT' == $request->getMethod()) {
$this->addPostFiles($guzzleRequest, $request->getFiles());
$requestOptions['headers'][$name] = $value;
}
// Let BrowserKit handle redirects
try {
$response = $this->getClient()->send($guzzleRequest);
$response = $this->getClient()->request($method, $uri, $requestOptions);
} catch (RequestException $e) {
$response = $e->getResponse();
if (null === $response) {
@ -139,33 +140,63 @@ class Client extends BaseClient
return $this->createResponse($response);
}
protected function addPostFiles(RequestInterface $request, array $files, $arrayName = '')
protected function addPostFiles(array $files, array &$multipart, $arrayName = '')
{
if (empty($files)) {
return;
}
foreach ($files as $name => $info) {
if (!empty($arrayName)) {
$name = $arrayName.'['.$name.']';
}
$file = [
'name' => $name,
];
if (is_array($info)) {
if (isset($info['tmp_name'])) {
if ('' !== $info['tmp_name']) {
$request->getBody()->addFile(new PostFile($name, fopen($info['tmp_name'], 'r'), isset($info['name']) ? $info['name'] : null));
$file['contents'] = fopen($info['tmp_name'], 'r');
if (isset($info['name'])) {
$file['filename'] = $info['name'];
}
} else {
continue;
}
} else {
$this->addPostFiles($request, $info, $name);
$this->addPostFiles($info, $multipart, $name);
continue;
}
} else {
$request->getBody()->addFile(new PostFile($name, fopen($info, 'r')));
$file['contents'] = fopen($info, 'r');
}
$multipart[] = $file;
}
}
public function addPostFields(array $formParams, array &$multipart, $arrayName = '')
{
foreach ($formParams as $name => $value) {
if (!empty($arrayName)) {
$name = $arrayName.'['.$name.']';
}
if (is_array($value)) {
$this->addPostFields($value, $multipart, $name);
} else {
$multipart[] = [
'name' => $name,
'contents' => $value,
];
}
}
}
protected function createResponse(GuzzleResponse $response)
protected function createResponse(ResponseInterface $response)
{
$headers = $response->getHeaders();
return new Response($response->getBody(true), $response->getStatusCode(), $headers);
return new Response((string) $response->getBody(), $response->getStatusCode(), $response->getHeaders());
}
}

View file

@ -14,31 +14,34 @@ namespace Goutte\Tests;
use Goutte\Client;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\Response as GuzzleResponse;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Subscriber\History;
use GuzzleHttp\Subscriber\Mock;
use GuzzleHttp\Post\PostFile;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response as GuzzleResponse;
use GuzzleHttp\Middleware;
use Symfony\Component\BrowserKit\Cookie;
/**
* Goutte Client Test
* Goutte Client Test.
*
* @author Michael Dowling <michael@guzzlephp.org>
* @author Charles Sarrazin <charles@sarraz.in>
*/
class ClientTest extends \PHPUnit_Framework_TestCase
{
protected $history;
/** @var MockHandler */
protected $mock;
protected function getGuzzle()
protected function getGuzzle(array $responses = [])
{
$this->history = new History();
$this->mock = new Mock();
$this->mock->addResponse(new GuzzleResponse(200, array(), Stream::factory('<html><body><p>Hi</p></body></html>')));
$guzzle = new GuzzleClient(array('redirect.disable' => true, 'base_url' => ''));
$guzzle->getEmitter()->attach($this->mock);
$guzzle->getEmitter()->attach($this->history);
if (empty($responses)) {
$responses = [new GuzzleResponse(200, [], '<html><body><p>Hi</p></body></html>')];
}
$this->mock = new MockHandler($responses);
$handlerStack = HandlerStack::create($this->mock);
$this->history = [];
$handlerStack->push(Middleware::history($this->history));
$guzzle = new GuzzleClient(array('redirect.disable' => true, 'base_uri' => '', 'handler' => $handlerStack));
return $guzzle;
}
@ -63,8 +66,8 @@ class ClientTest extends \PHPUnit_Framework_TestCase
$client = new Client();
$client->setClient($guzzle);
$client->setHeader('X-Test', 'test');
$crawler = $client->request('GET', 'http://www.example.com/');
$this->assertEquals('test', $this->history->getLastRequest()->getHeader('X-Test'));
$client->request('GET', 'http://www.example.com/');
$this->assertEquals('test', end($this->history)['request']->getHeaderLine('X-Test'));
}
public function testCustomUserAgent()
@ -73,8 +76,8 @@ class ClientTest extends \PHPUnit_Framework_TestCase
$client = new Client();
$client->setClient($guzzle);
$client->setHeader('User-Agent', 'foo');
$crawler = $client->request('GET', 'http://www.example.com/');
$this->assertEquals('foo', $this->history->getLastRequest()->getHeader('User-Agent'));
$client->request('GET', 'http://www.example.com/');
$this->assertEquals('Symfony2 BrowserKit, foo', end($this->history)['request']->getHeaderLine('User-Agent'));
}
public function testUsesAuth()
@ -83,11 +86,9 @@ class ClientTest extends \PHPUnit_Framework_TestCase
$client = new Client();
$client->setClient($guzzle);
$client->setAuth('me', '**');
$crawler = $client->request('GET', 'http://www.example.com/');
$request = $this->history->getLastRequest();
$this->assertEquals('me', $request->getConfig()->get('auth')[0]);
$this->assertEquals('**', $request->getConfig()->get('auth')[1]);
$this->assertEquals('basic', $request->getConfig()->get('auth')[2]);
$client->request('GET', 'http://www.example.com/');
$request = end($this->history)['request'];
$this->assertEquals('Basic bWU6Kio=', $request->getHeaderLine('Authorization'));
}
public function testResetsAuth()
@ -97,10 +98,9 @@ class ClientTest extends \PHPUnit_Framework_TestCase
$client->setClient($guzzle);
$client->setAuth('me', '**');
$client->resetAuth();
$crawler = $client->request('GET', 'http://www.example.com/');
$request = $this->history->getLastRequest();
$this->assertNull($request->getConfig()->get('auth')[0]);
$this->assertNull($request->getConfig()->get('auth')[1]);
$client->request('GET', 'http://www.example.com/');
$request = end($this->history)['request'];
$this->assertEquals('', $request->getHeaderLine('authorization'));
}
public function testUsesCookies()
@ -109,9 +109,9 @@ class ClientTest extends \PHPUnit_Framework_TestCase
$client = new Client();
$client->setClient($guzzle);
$client->getCookieJar()->set(new Cookie('test', '123'));
$crawler = $client->request('GET', 'http://www.example.com/');
$request = $this->history->getLastRequest();
$this->assertEquals('test=123', $request->getHeader('Cookie'));
$client->request('GET', 'http://www.example.com/');
$request = end($this->history)['request'];
$this->assertEquals('test=123', $request->getHeaderLine('Cookie'));
}
public function testUsesPostFiles()
@ -122,18 +122,20 @@ class ClientTest extends \PHPUnit_Framework_TestCase
$files = array(
'test' => array(
'name' => 'test.txt',
'tmp_name' => __FILE__,
'tmp_name' => __DIR__.'/fixtures.txt',
),
);
$crawler = $client->request('POST', 'http://www.example.com/', array(), $files);
$request = $this->history->getLastRequest();
$client->request('POST', 'http://www.example.com/', array(), $files);
$request = end($this->history)['request'];
$files = $request->getBody()->getFiles();
$this->assertFile(reset($files), 'test', 'test.txt', array(
'Content-Type' => 'text/plain',
'Content-Disposition' => 'form-data; filename="test.txt"; name="test"',
));
$stream = $request->getBody();
$boundary = $stream->getBoundary();
$this->assertEquals(
"--$boundary\r\nContent-Disposition: form-data; name=\"test\"; filename=\"test.txt\"\r\nContent-Length: 4\r\n"
."Content-Type: text/plain\r\n\r\nfoo\n\r\n--$boundary--\r\n",
$stream->getContents()
);
}
public function testUsesPostNamedFiles()
@ -142,16 +144,19 @@ class ClientTest extends \PHPUnit_Framework_TestCase
$client = new Client();
$client->setClient($guzzle);
$files = array(
'test' => __FILE__,
'test' => __DIR__.'/fixtures.txt',
);
$crawler = $client->request('POST', 'http://www.example.com/', array(), $files);
$request = $this->history->getLastRequest();
$files = $request->getBody()->getFiles();
$this->assertFile(reset($files), 'test', __FILE__, array(
'Content-Type' => 'text/x-php',
'Content-Disposition' => 'form-data; filename="ClientTest.php"; name="test"',
));
$client->request('POST', 'http://www.example.com/', array(), $files);
$request = end($this->history)['request'];
$stream = $request->getBody();
$boundary = $stream->getBoundary();
$this->assertEquals(
"--$boundary\r\nContent-Disposition: form-data; name=\"test\"; filename=\"fixtures.txt\"\r\nContent-Length: 4\r\n"
."Content-Type: text/plain\r\n\r\nfoo\n\r\n--$boundary--\r\n",
$stream->getContents()
);
}
public function testUsesPostFilesNestedFields()
@ -163,18 +168,73 @@ class ClientTest extends \PHPUnit_Framework_TestCase
'form' => array(
'test' => array(
'name' => 'test.txt',
'tmp_name' => __FILE__,
'tmp_name' => __DIR__.'/fixtures.txt',
),
),
);
$crawler = $client->request('POST', 'http://www.example.com/', array(), $files);
$request = $this->history->getLastRequest();
$files = $request->getBody()->getFiles();
$this->assertFile(reset($files), 'form[test]', 'test.txt', array(
'Content-Type' => 'text/plain',
'Content-Disposition' => 'form-data; filename="test.txt"; name="form[test]"',
));
$client->request('POST', 'http://www.example.com/', array(), $files);
$request = end($this->history)['request'];
$stream = $request->getBody();
$boundary = $stream->getBoundary();
$this->assertEquals(
"--$boundary\r\nContent-Disposition: form-data; name=\"form[test]\"; filename=\"test.txt\"\r\nContent-Length: 4\r\n"
."Content-Type: text/plain\r\n\r\nfoo\n\r\n--$boundary--\r\n",
$stream->getContents()
);
}
public function testPostFormWithFiles()
{
$guzzle = $this->getGuzzle();
$client = new Client();
$client->setClient($guzzle);
$files = array(
'test' => __DIR__.'/fixtures.txt',
);
$params = array(
'foo' => 'bar',
);
$client->request('POST', 'http://www.example.com/', $params, $files);
$request = end($this->history)['request'];
$stream = $request->getBody();
$boundary = $stream->getBoundary();
$this->assertEquals(
"--$boundary\r\nContent-Disposition: form-data; name=\"foo\"\r\nContent-Length: 3\r\n"
."\r\nbar\r\n"
."--$boundary\r\nContent-Disposition: form-data; name=\"test\"; filename=\"fixtures.txt\"\r\nContent-Length: 4\r\n"
."Content-Type: text/plain\r\n\r\nfoo\n\r\n--$boundary--\r\n",
$stream->getContents());
}
public function testPostEmbeddedFormWithFiles()
{
$guzzle = $this->getGuzzle();
$client = new Client();
$client->setClient($guzzle);
$files = array(
'test' => __DIR__.'/fixtures.txt',
);
$params = array(
'foo' => array(
'bar' => 'baz',
),
);
$client->request('POST', 'http://www.example.com/', $params, $files);
$request = end($this->history)['request'];
$stream = $request->getBody();
$boundary = $stream->getBoundary();
$this->assertEquals(
"--$boundary\r\nContent-Disposition: form-data; name=\"foo[bar]\"\r\nContent-Length: 3\r\n"
."\r\nbaz\r\n"
."--$boundary\r\nContent-Disposition: form-data; name=\"test\"; filename=\"fixtures.txt\"\r\nContent-Length: 4\r\n"
."Content-Type: text/plain\r\n\r\nfoo\n\r\n--$boundary--\r\n",
$stream->getContents());
}
public function testUsesPostFilesOnClientSide()
@ -183,16 +243,19 @@ class ClientTest extends \PHPUnit_Framework_TestCase
$client = new Client();
$client->setClient($guzzle);
$files = array(
'test' => __FILE__,
'test' => __DIR__.'/fixtures.txt',
);
$crawler = $client->request('POST', 'http://www.example.com/', array(), $files);
$request = $this->history->getLastRequest();
$files = $request->getBody()->getFiles();
$this->assertFile(reset($files), 'test', __FILE__, array(
'Content-Type' => 'text/x-php',
'Content-Disposition' => 'form-data; filename="ClientTest.php"; name="test"',
));
$client->request('POST', 'http://www.example.com/', array(), $files);
$request = end($this->history)['request'];
$stream = $request->getBody();
$boundary = $stream->getBoundary();
$this->assertEquals(
"--$boundary\r\nContent-Disposition: form-data; name=\"test\"; filename=\"fixtures.txt\"\r\nContent-Length: 4\r\n"
."Content-Type: text/plain\r\n\r\nfoo\n\r\n--$boundary--\r\n",
$stream->getContents()
);
}
public function testUsesPostFilesUploadError()
@ -210,10 +273,12 @@ class ClientTest extends \PHPUnit_Framework_TestCase
),
);
$crawler = $client->request('POST', 'http://www.example.com/', array(), $files);
$request = $this->history->getLastRequest();
$client->request('POST', 'http://www.example.com/', array(), $files);
$request = end($this->history)['request'];
$stream = $request->getBody();
$boundary = $stream->getBoundary();
$this->assertEquals(array(), $request->getBody()->getFiles());
$this->assertEquals("--$boundary--\r\n", $stream->getContents());
}
public function testCreatesResponse()
@ -227,13 +292,12 @@ class ClientTest extends \PHPUnit_Framework_TestCase
public function testHandlesRedirectsCorrectly()
{
$guzzle = $this->getGuzzle();
$this->mock->clearQueue();
$this->mock->addResponse(new GuzzleResponse(301, array(
'Location' => 'http://www.example.com/',
)));
$this->mock->addResponse(new GuzzleResponse(200, [], Stream::factory('<html><body><p>Test</p></body></html>')));
$guzzle = $this->getGuzzle([
new GuzzleResponse(301, array(
'Location' => 'http://www.example.com/',
)),
new GuzzleResponse(200, [], '<html><body><p>Test</p></body></html>'),
]);
$client = new Client();
$client->setClient($guzzle);
@ -247,12 +311,11 @@ class ClientTest extends \PHPUnit_Framework_TestCase
public function testConvertsGuzzleHeadersToArrays()
{
$guzzle = $this->getGuzzle();
$this->mock->clearQueue();
$this->mock->addResponse(new GuzzleResponse(200, array(
'Date' => 'Tue, 04 Jun 2013 13:22:41 GMT',
)));
$guzzle = $this->getGuzzle([
new GuzzleResponse(200, array(
'Date' => 'Tue, 04 Jun 2013 13:22:41 GMT',
)),
]);
$client = new Client();
$client->setClient($guzzle);
@ -260,48 +323,30 @@ class ClientTest extends \PHPUnit_Framework_TestCase
$response = $client->getResponse();
$headers = $response->getHeaders();
$this->assertInternalType("array", array_shift($headers), "Header not converted from Guzzle\Http\Message\Header to array");
$this->assertInternalType('array', array_shift($headers), 'Header not converted from Guzzle\Http\Message\Header to array');
}
public function testNullResponseException()
{
$this->setExpectedException('GuzzleHttp\Exception\RequestException');
$guzzle = $this->getGuzzle();
$this->mock->clearQueue();
$exception = new RequestException('', $this->getMock('GuzzleHttp\Message\RequestInterface'));
$this->mock->addException($exception);
$guzzle = $this->getGuzzle([
new RequestException('', $this->getMock('Psr\Http\Message\RequestInterface')),
]);
$client = new Client();
$client->setClient($guzzle);
$client->request('GET', 'http://www.example.com/');
$response = $client->getResponse();
}
protected function assertFile(PostFile $postFile, $fieldName, $fileName, $headers)
{
$this->assertEquals($postFile->getName(), $fieldName);
$this->assertEquals($postFile->getFilename(), $fileName);
$postFileHeaders = $postFile->getHeaders();
// Note: Sort 'Content-Disposition' values before comparing, because the order changed in Guzzle 4.2.2
$postFileHeaders['Content-Disposition'] = explode('; ', $postFileHeaders['Content-Disposition']);
sort($postFileHeaders['Content-Disposition']);
$headers['Content-Disposition'] = explode('; ', $headers['Content-Disposition']);
sort($headers['Content-Disposition']);
$this->assertEquals($postFileHeaders, $headers);
$client->getResponse();
}
public function testHttps()
{
$guzzle = $this->getGuzzle();
$guzzle = $this->getGuzzle([
new GuzzleResponse(200, [], '<html><body><p>Test</p></body></html>'),
]);
$this->mock->clearQueue();
$this->mock->addResponse(new GuzzleResponse(200, [], Stream::factory('<html><body><p>Test</p></body></html>')));
$client = new Client();
$client->setClient($guzzle);
$crawler = $client->request('GET', 'https://www.example.com/');
$this->assertEquals('https', $this->history->getLastRequest()->getScheme());
$this->assertEquals('Test', $crawler->filter('p')->text());
}
@ -313,7 +358,7 @@ class ClientTest extends \PHPUnit_Framework_TestCase
'HTTP_USER_AGENT' => 'SomeHost',
]);
$client->setClient($guzzle);
$crawler = $client->request('GET', 'http://www.example.com/');
$this->assertEquals('SomeHost', $this->history->getLastRequest()->getHeader('User-Agent'));
$client->request('GET', 'http://www.example.com/');
$this->assertEquals('SomeHost', end($this->history)['request']->getHeaderLine('User-Agent'));
}
}

View file

@ -0,0 +1 @@
foo

View file

@ -9,11 +9,12 @@ responses.
Requirements
------------
Goutte depends on PHP 5.4+ and Guzzle 4+.
Goutte depends on PHP 5.5+ and Guzzle 6+.
.. tip::
If you need support for PHP 5.3 or Guzzle 3, use Goutte 1.0.6.
If you need support for PHP 5.4 or Guzzle 4-5, use Goutte 2.x.
If you need support for PHP 5.3 or Guzzle 3, use Goutte 1.x.
Installation
------------
@ -22,7 +23,7 @@ Add ``fabpot/goutte`` as a require dependency in your ``composer.json`` file:
.. code-block:: bash
php composer.phar require fabpot/goutte:~2.0
composer require fabpot/goutte
.. tip::
@ -32,6 +33,9 @@ Add ``fabpot/goutte`` as a require dependency in your ``composer.json`` file:
require_once '/path/to/goutte.phar';
The phars for Goutte 1.x are also available for `download
<http://get.sensiolabs.org/goutte-v1.0.7.phar>`.
Usage
-----
@ -73,7 +77,7 @@ Extract data:
.. code-block:: php
// Get the latest post in this category and display the titles
$crawler->filter('h2.post > a')->each(function ($node) {
$crawler->filter('h2 > a')->each(function ($node) {
print $node->text()."\n";
});
@ -92,8 +96,14 @@ Submit forms:
More Information
----------------
Read the documentation of the BrowserKit and DomCrawler Symfony Components for
more information about what you can do with Goutte.
Read the documentation of the BrowserKit and `DomCrawler
<http://symfony.com/doc/any/components/dom_crawler.html>`_ Symfony Components
for more information about what you can do with Goutte.
Pronunciation
-------------
Goutte is pronounced ``goot`` i.e. it rhymes with ``boot`` and not ``out``.
Technical Information
---------------------

View file

@ -12,18 +12,18 @@
}
],
"require": {
"php": ">=5.4.0",
"php": ">=5.5.0",
"symfony/browser-kit": "~2.1",
"symfony/css-selector": "~2.1",
"symfony/dom-crawler": "~2.1",
"guzzlehttp/guzzle": ">=4,<6"
"guzzlehttp/guzzle": "^6.0"
},
"autoload": {
"psr-4": { "Goutte\\": "Goutte" }
},
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
"dev-master": "3.1-dev"
}
}
}

View file

@ -1,11 +0,0 @@
phpunit.xml
composer.phar
composer.lock
composer-test.lock
vendor/
build/artifacts/
artifacts/
docs/_build
docs/*.pyc
.idea
.DS_STORE

View file

@ -1,15 +1,13 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- hhvm
before_script:
- curl --version
- pear config-set php_ini ~/.phpenv/versions/`php -r 'echo phpversion();'`/etc/php.ini || echo 'Error modifying PEAR'
- pecl install uri_template || echo 'Error installing uri_template'
- composer self-update
- composer install --no-interaction --prefer-source --dev
- ~/.nvm/nvm.sh install v0.6.14
@ -20,6 +18,7 @@ script: make test
matrix:
allow_failures:
- php: hhvm
- php: 7.0
fast_finish: true
before_deploy:
@ -36,4 +35,4 @@ deploy:
repo: guzzle/guzzle
tags: true
all_branches: true
php: 5.4
php: 5.5

View file

@ -1,5 +1,116 @@
# CHANGELOG
## 6.0.2 - 2015-07-04
* Fixed a memory leak in the curl handlers in which references to callbacks
were not being removed by `curl_reset`.
* Cookies are now extracted properly before redirects.
* Cookies now allow more character ranges.
* Decoded Content-Encoding responses are now modified to correctly reflect
their state if the encoding was automatically removed by a handler. This
means that the `Content-Encoding` header may be removed an the
`Content-Length` modified to reflect the message size after removing the
encoding.
* Added a more explicit error message when trying to use `form_params` and
`multipart` in the same request.
* Several fixes for HHVM support.
* Functions are now conditionally required using an additional level of
indirection to help with global Composer installations.
## 6.0.1 - 2015-05-27
* Fixed a bug with serializing the `query` request option where the `&`
separator was missing.
* Added a better error message for when `body` is provided as an array. Please
use `form_params` or `multipart` instead.
* Various doc fixes.
## 6.0.0 - 2015-05-26
* See the UPGRADING.md document for more information.
* Added `multipart` and `form_params` request options.
* Added `synchronous` request option.
* Added the `on_headers` request option.
* Fixed `expect` handling.
* No longer adding default middlewares in the client ctor. These need to be
present on the provided handler in order to work.
* Requests are no longer initiated when sending async requests with the
CurlMultiHandler. This prevents unexpected recursion from requests completing
while ticking the cURL loop.
* Removed the semantics of setting `default` to `true`. This is no longer
required now that the cURL loop is not ticked for async requests.
* Added request and response logging middleware.
* No longer allowing self signed certificates when using the StreamHandler.
* Ensuring that `sink` is valid if saving to a file.
* Request exceptions now include a "handler context" which provides handler
specific contextual information.
* Added `GuzzleHttp\RequestOptions` to allow request options to be applied
using constants.
* `$maxHandles` has been removed from CurlMultiHandler.
* `MultipartPostBody` is now part of the `guzzlehttp/psr7` package.
## 5.3.0 - 2015-05-19
* Mock now supports `save_to`
* Marked `AbstractRequestEvent::getTransaction()` as public.
* Fixed a bug in which multiple headers using different casing would overwrite
previous headers in the associative array.
* Added `Utils::getDefaultHandler()`
* Marked `GuzzleHttp\Client::getDefaultUserAgent` as deprecated.
* URL scheme is now always lowercased.
## 6.0.0-beta.1
* Requires PHP >= 5.5
* Updated to use PSR-7
* Requires immutable messages, which basically means an event based system
owned by a request instance is no longer possible.
* Utilizing the [Guzzle PSR-7 package](https://github.com/guzzle/psr7).
* Removed the dependency on `guzzlehttp/streams`. These stream abstractions
are available in the `guzzlehttp/psr7` package under the `GuzzleHttp\Psr7`
namespace.
* Added middleware and handler system
* Replaced the Guzzle event and subscriber system with a middleware system.
* No longer depends on RingPHP, but rather places the HTTP handlers directly
in Guzzle, operating on PSR-7 messages.
* Retry logic is now encapsulated in `GuzzleHttp\Middleware::retry`, which
means the `guzzlehttp/retry-subscriber` is now obsolete.
* Mocking responses is now handled using `GuzzleHttp\Handler\MockHandler`.
* Asynchronous responses
* No longer supports the `future` request option to send an async request.
Instead, use one of the `*Async` methods of a client (e.g., `requestAsync`,
`getAsync`, etc.).
* Utilizing `GuzzleHttp\Promise` instead of React's promise library to avoid
recursion required by chaining and forwarding react promises. See
https://github.com/guzzle/promises
* Added `requestAsync` and `sendAsync` to send request asynchronously.
* Added magic methods for `getAsync()`, `postAsync()`, etc. to send requests
asynchronously.
* Request options
* POST and form updates
* Added the `form_fields` and `form_files` request options.
* Removed the `GuzzleHttp\Post` namespace.
* The `body` request option no longer accepts an array for POST requests.
* The `exceptions` request option has been deprecated in favor of the
`http_errors` request options.
* The `save_to` request option has been deprecated in favor of `sink` request
option.
* Clients no longer accept an array of URI template string and variables for
URI variables. You will need to expand URI templates before passing them
into a client constructor or request method.
* Client methods `get()`, `post()`, `put()`, `patch()`, `options()`, etc. are
now magic methods that will send synchronous requests.
* Replaced `Utils.php` with plain functions in `functions.php`.
* Removed `GuzzleHttp\Collection`.
* Removed `GuzzleHttp\BatchResults`. Batched pool results are now returned as
an array.
* Removed `GuzzleHttp\Query`. Query string handling is now handled using an
associative array passed into the `query` request option. The query string
is serialized using PHP's `http_build_query`. If you need more control, you
can pass the query string in as a string.
* `GuzzleHttp\QueryParser` has been replaced with the
`GuzzleHttp\Psr7\parse_query`.
## 5.2.0 - 2015-01-27
* Added `AppliesHeadersInterface` to make applying headers to a request based

View file

@ -1,4 +1,4 @@
Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling <mtdowling@gmail.com>
Copyright (c) 2011-2015 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

View file

@ -1,50 +0,0 @@
all: clean coverage docs
start-server:
cd vendor/guzzlehttp/ringphp && make start-server
stop-server:
cd vendor/guzzlehttp/ringphp && make stop-server
test: start-server
vendor/bin/phpunit
$(MAKE) stop-server
coverage: start-server
vendor/bin/phpunit --coverage-html=artifacts/coverage
$(MAKE) stop-server
view-coverage:
open artifacts/coverage/index.html
clean:
rm -rf artifacts/*
docs:
cd docs && make html && cd ..
view-docs:
open docs/_build/html/index.html
tag:
$(if $(TAG),,$(error TAG is not defined. Pass via "make tag TAG=4.2.1"))
@echo Tagging $(TAG)
chag update $(TAG)
sed -i '' -e "s/VERSION = '.*'/VERSION = '$(TAG)'/" src/ClientInterface.php
php -l src/ClientInterface.php
git add -A
git commit -m '$(TAG) release'
chag tag
perf: start-server
php tests/perf.php
$(MAKE) stop-server
package: burgomaster
php build/packager.php
burgomaster:
mkdir -p build/artifacts
curl -s https://raw.githubusercontent.com/mtdowling/Burgomaster/0.0.2/src/Burgomaster.php > build/artifacts/Burgomaster.php
.PHONY: docs burgomaster

View file

@ -1,25 +1,24 @@
Guzzle, PHP HTTP client and webservice framework
================================================
Guzzle, PHP HTTP client
=======================
[![Build Status](https://secure.travis-ci.org/guzzle/guzzle.png?branch=master)](http://travis-ci.org/guzzle/guzzle)
[![Build Status](https://secure.travis-ci.org/guzzle/guzzle.svg?branch=master)](http://travis-ci.org/guzzle/guzzle)
Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and
trivial to integrate with web services.
- Manages things like persistent connections, represents query strings as
collections, simplifies sending streaming POST requests with fields and
files, and abstracts away the underlying HTTP transport layer.
- Can send both synchronous and asynchronous requests using the same interface
without requiring a dependency on a specific event loop.
- Pluggable HTTP adapters allows Guzzle to integrate with any method you choose
for sending HTTP requests over the wire (e.g., cURL, sockets, PHP's stream
wrapper, non-blocking event loops like ReactPHP.
- Guzzle makes it so that you no longer need to fool around with cURL options,
stream contexts, or sockets.
- 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();
$response = $client->get('http://guzzlephp.org');
$res = $client->get('https://api.github.com/user', ['auth' => ['user', 'pass']]);
echo $res->getStatusCode();
// "200"
@ -27,22 +26,23 @@ echo $res->getHeader('content-type');
// 'application/json; charset=utf8'
echo $res->getBody();
// {"type":"User"...'
var_export($res->json());
// Outputs the JSON decoded data
// Send an asynchronous request.
$req = $client->createRequest('GET', 'http://httpbin.org', ['future' => true]);
$client->send($req)->then(function ($response) {
echo 'I completed! ' . $response;
$request = new \GuzzleHttp\Psr7\Request('GET', 'http://httpbin.org');
$promise = $client->sendAsync($request)->then(function ($response) {
echo 'I completed! ' . $response->getBody();
});
$promise->wait();
```
Get more information and answers with the
[Documentation](http://guzzlephp.org/),
[Forums](https://groups.google.com/forum/?hl=en#!forum/guzzle),
and [Gitter](https://gitter.im/guzzle/guzzle).
## Help and docs
### Installing via Composer
- [Documentation](http://guzzlephp.org/)
- [stackoverflow](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).
@ -55,7 +55,7 @@ curl -sS https://getcomposer.org/installer | php
Next, run the Composer command to install the latest stable version of Guzzle:
```bash
composer require guzzlehttp/guzzle
composer.phar require guzzlehttp/guzzle
```
After installing, you need to require Composer's autoloader:
@ -64,7 +64,25 @@ After installing, you need to require Composer's autoloader:
require 'vendor/autoload.php';
```
### Documentation
You can then later update Guzzle using composer:
More information can be found in the online documentation at
http://guzzlephp.org/.
```bash
composer.phar update
```
## Version Guidance
| Version | Status | Packagist | Namespace | Repo | Docs | PSR-7 |
|---------|-------------|---------------------|--------------|---------------------|---------------------|-------|
| 3.x | EOL | `guzzle/guzzle` | `Guzzle` | [v3][guzzle-3-repo] | [v3][guzzle-3-docs] | No |
| 4.x | EOL | `guzzlehttp/guzzle` | `GuzzleHttp` | N/A | N/A | No |
| 5.x | Maintained | `guzzlehttp/guzzle` | `GuzzleHttp` | [v5][guzzle-5-repo] | [v5][guzzle-5-docs] | No |
| 6.x | Latest | `guzzlehttp/guzzle` | `GuzzleHttp` | [v6][guzzle-6-repo] | [v6][guzzle-6-docs] | Yes |
[guzzle-3-repo]: https://github.com/guzzle/guzzle3
[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/

View file

@ -1,12 +1,161 @@
Guzzle Upgrade Guide
====================
5.0 to 6.0
----------
Guzzle now uses [PSR-7](http://www.php-fig.org/psr/psr-7/) for HTTP messages.
Due to the fact that these messages are immutable, this prompted a refactoring
of Guzzle to use a middleware based system rather than an event system. Any
HTTP message interaction (e.g., `GuzzleHttp\Message\Request`) need to be
updated to work with the new immutable PSR-7 request and response objects. Any
event listeners or subscribers need to be updated to become middleware
functions that wrap handlers (or are injected into a
`GuzzleHttp\HandlerStack`).
- Removed `GuzzleHttp\BatchResults`
- Removed `GuzzleHttp\Collection`
- Removed `GuzzleHttp\HasDataTrait`
- Removed `GuzzleHttp\ToArrayInterface`
- The `guzzlehttp/streams` dependency has been removed. Stream functionality
is now present in the `GuzzleHttp\Psr7` namespace provided by the
`guzzlehttp/psr7` package.
- Guzzle no longer uses ReactPHP promises and now uses the
`guzzlehttp/promises` library. We use a custom promise library for three
significant reasons:
1. React promises (at the time of writing this) are recursive. Promise
chaining and promise resolution will eventually blow the stack. Guzzle
promises are not recursive as they use a sort of trampolining technique.
Note: there has been movement in the React project to modify promises to
no longer utilize recursion.
2. Guzzle needs to have the ability to synchronously block on a promise to
wait for a result. Guzzle promises allows this functionality (and does
not require the use of recursion).
3. Because we need to be able to wait on a result, doing so using React
promises requires wrapping react promises with RingPHP futures. This
overhead is no longer needed, reducing stack sizes, reducing complexity,
and improving performance.
- `GuzzleHttp\Mimetypes` has been moved to a function in
`GuzzleHttp\Psr7\mimetype_from_extension` and
`GuzzleHttp\Psr7\mimetype_from_filename`.
- `GuzzleHttp\Query` and `GuzzleHttp\QueryParser` have been removed. Query
strings must now be passed into request objects as strings, or provided to
the `query` request option when creating requests with clients. The `query`
option uses PHP's `http_build_query` to convert an array to a string. If you
need a different serialization technique, you will need to pass the query
string in as a string. There are a couple helper functions that will make
working with query strings easier: `GuzzleHttp\Psr7\parse_query` and
`GuzzleHttp\Psr7\build_query`.
- Guzzle no longer has a dependency on RingPHP. Due to the use of a middleware
system based on PSR-7, using RingPHP and it's middleware system as well adds
more complexity than the benefits it provides. All HTTP handlers that were
present in RingPHP have been modified to work directly with PSR-7 messages
and placed in the `GuzzleHttp\Handler` namespace. This significantly reduces
complexity in Guzzle, removes a dependency, and improves performance. RingPHP
will be maintained for Guzzle 5 support, but will no longer be a part of
Guzzle 6.
- As Guzzle now uses a middleware based systems the event system and RingPHP
integration has been removed. Note: while the event system has been removed,
it is possible to add your own type of event system that is powered by the
middleware system.
- Removed the `Event` namespace.
- Removed the `Subscriber` namespace.
- Removed `Transaction` class
- Removed `RequestFsm`
- Removed `RingBridge`
- `GuzzleHttp\Subscriber\Cookie` is now provided by
`GuzzleHttp\Middleware::cookies`
- `GuzzleHttp\Subscriber\HttpError` is now provided by
`GuzzleHttp\Middleware::httpError`
- `GuzzleHttp\Subscriber\History` is now provided by
`GuzzleHttp\Middleware::history`
- `GuzzleHttp\Subscriber\Mock` is now provided by
`GuzzleHttp\Handler\MockHandler`
- `GuzzleHttp\Subscriber\Prepare` is now provided by
`GuzzleHttp\PrepareBodyMiddleware`
- `GuzzleHttp\Subscriber\Redirect` is now provided by
`GuzzleHttp\RedirectMiddleware`
- Guzzle now uses `Psr\Http\Message\UriInterface` (implements in
`GuzzleHttp\Psr7\Uri`) for URI support. `GuzzleHttp\Url` is now gone.
- Static functions in `GuzzleHttp\Utils` have been moved to namespaced
functions under the `GuzzleHttp` namespace. This requires either a Composer
based autoloader or you to include functions.php.
- `GuzzleHttp\ClientInterface::getDefaultOption` has been renamed to
`GuzzleHttp\ClientInterface::getConfig`.
- `GuzzleHttp\ClientInterface::setDefaultOption` has been removed.
## Migrating to middleware
The change to PSR-7 unfortunately required significant refactoring to Guzzle
due to the fact that PSR-7 messages are immutable. Guzzle 5 relied on an event
system from plugins. The event system relied on mutability of HTTP messages and
side effects in order to work. With immutable messages, you have to change your
workflow to become more about either returning a value (e.g., functional
middlewares) or setting a value on an object. Guzzle v6 has chosen the
functional middleware approach.
Instead of using the event system to listen for things like the `before` event,
you now create a stack based middleware function that intercepts a request on
the way in and the promise of the response on the way out. This is a much
simpler and more predictable approach than the event system and works nicely
with PSR-7 middleware. Due to the use of promises, the middleware system is
also asynchronous.
v5:
```php
use GuzzleHttp\Event\BeforeEvent;
$client = new GuzzleHttp\Client();
// Get the emitter and listen to the before event.
$client->getEmitter()->on('before', function (BeforeEvent $e) {
// Guzzle v5 events relied on mutation
$e->getRequest()->setHeader('X-Foo', 'Bar');
});
```
v6:
In v6, you can modify the request before it is sent using the `mapRequest`
middleware. The idiomatic way in v6 to modify the request/response lifecycle is
to setup a handler middleware stack up front and inject the handler into a
client.
```php
use GuzzleHttp\Middleware;
// Create a handler stack that has all of the default middlewares attached
$handler = GuzzleHttp\HandlerStack::create();
// Push the handler onto the handler stack
$handler->push(Middleware::mapRequest(function (RequestInterface $request) {
// Notice that we have to return a request object
return $request->withHeader('X-Foo', 'Bar');
});
// Inject the handler into the client
$client = new GuzzleHttp\Client(['handler' => $handler]);
```
## POST Requests
This version added the [`form_params`](http://guzzle.readthedocs.org/en/latest/request-options.html#form_params)
and `multipart` request options. `form_params` is an associative array of
strings or array of strings and is used to serialize an
`application/x-www-form-urlencoded` POST request. The
[`multipart`](http://guzzle.readthedocs.org/en/latest/request-options.html#multipart)
option is now used to send a multipart/form-data POST request.
`GuzzleHttp\Post\PostFile` has been removed. Use the `multipart` option to add
POST files to a multipart/form-data request.
The `body` option no longer accepts an array to send POST requests. Please use
`multipart` or `form_params` instead.
The `base_url` option has been renamed to `base_uri`.
4.x to 5.0
----------
## Rewritten Adapter Layer
Guzzle now uses `RingPHP <http://ringphp.readthedocs.org/en/latest/>`_ to send
Guzzle now uses [RingPHP](http://ringphp.readthedocs.org/en/latest) to send
HTTP requests. The `adapter` option in a `GuzzleHttp\Client` constructor
is still supported, but it has now been renamed to `handler`. Instead of
passing a `GuzzleHttp\Adapter\AdapterInterface`, you must now pass a PHP
@ -14,7 +163,7 @@ passing a `GuzzleHttp\Adapter\AdapterInterface`, you must now pass a PHP
## Removed Fluent Interfaces
`Fluent interfaces were removed <http://ocramius.github.io/blog/fluent-interfaces-are-evil/>`_
[Fluent interfaces were removed](http://ocramius.github.io/blog/fluent-interfaces-are-evil)
from the following classes:
- `GuzzleHttp\Collection`
@ -35,7 +184,7 @@ functions can be used as replacements.
deprecated in favor of using `GuzzleHttp\Pool::batch()`.
The "procedural" global client has been removed with no replacement (e.g.,
`GuzzleHttp\get()`, `GuzzleHttp\post()`, etc.). Use a `GuzzleHttl\Client`
`GuzzleHttp\get()`, `GuzzleHttp\post()`, etc.). Use a `GuzzleHttp\Client`
object as a replacement.
## `throwImmediately` has been removed

View file

@ -1,21 +0,0 @@
<?php
require __DIR__ . '/artifacts/Burgomaster.php';
$stageDirectory = __DIR__ . '/artifacts/staging';
$projectRoot = __DIR__ . '/../';
$packager = new \Burgomaster($stageDirectory, $projectRoot);
// Copy basic files to the stage directory. Note that we have chdir'd onto
// the $projectRoot directory, so use relative paths.
foreach (['README.md', 'LICENSE'] as $file) {
$packager->deepCopy($file, $file);
}
// Copy each dependency to the staging directory. Copy *.php and *.pem files.
$packager->recursiveCopy('src', 'GuzzleHttp', ['php']);
$packager->recursiveCopy('vendor/react/promise/src', 'React/Promise');
$packager->recursiveCopy('vendor/guzzlehttp/ringphp/src', 'GuzzleHttp/Ring');
$packager->recursiveCopy('vendor/guzzlehttp/streams/src', 'GuzzleHttp/Stream');
$packager->createAutoloader(['React/Promise/functions.php']);
$packager->createPhar(__DIR__ . '/artifacts/guzzle.phar');
$packager->createZip(__DIR__ . '/artifacts/guzzle.zip');

View file

@ -1,7 +1,7 @@
{
"name": "guzzlehttp/guzzle",
"type": "library",
"description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients",
"description": "Guzzle is a PHP HTTP client library",
"keywords": ["framework", "http", "rest", "web service", "curl", "client", "HTTP client"],
"homepage": "http://guzzlephp.org/",
"license": "MIT",
@ -13,15 +13,17 @@
}
],
"require": {
"php": ">=5.4.0",
"guzzlehttp/ringphp": "~1.0"
"php": ">=5.5.0",
"guzzlehttp/psr7": "~1.1",
"guzzlehttp/promises": "~1.0"
},
"require-dev": {
"ext-curl": "*",
"psr/log": "~1.0",
"phpunit/phpunit": "~4.0"
"phpunit/phpunit": "~4.0",
"psr/log": "~1.0"
},
"autoload": {
"files": ["src/functions_include.php"],
"psr-4": {
"GuzzleHttp\\": "src/"
}
@ -33,7 +35,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
"dev-master": "6.0-dev"
}
}
}

View file

@ -1,153 +0,0 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Guzzle.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Guzzle.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Guzzle"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Guzzle"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

Binary file not shown.

Before

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

View file

@ -1,3 +0,0 @@
<li><a href="https://github.com/guzzle/guzzle">GitHub</a></li>
<li><a href="https://groups.google.com/forum/?hl=en#!forum/guzzle">Forum</a></li>
<li><a href="irc:irc.freenode.com/#guzzlephp">IRC</a></li>

File diff suppressed because it is too large Load diff

View file

@ -1,28 +0,0 @@
import sys, os
from sphinx.highlighting import lexers
from pygments.lexers.web import PhpLexer
lexers['php'] = PhpLexer(startinline=True, linenos=1)
lexers['php-annotations'] = PhpLexer(startinline=True, linenos=1)
primary_domain = 'php'
extensions = []
templates_path = ['_templates']
source_suffix = '.rst'
master_doc = 'index'
project = u'Guzzle'
copyright = u'2014, Michael Dowling'
version = '5.0.0'
html_title = "Guzzle Documentation"
html_short_title = "Guzzle"
exclude_patterns = ['_build']
html_static_path = ['_static']
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if not on_rtd: # only import and set the theme if we're building docs locally
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]

View file

@ -1,520 +0,0 @@
============
Event System
============
Guzzle uses an event emitter to allow you to easily extend the behavior of a
request, change the response associated with a request, and implement custom
error handling. All events in Guzzle are managed and emitted by an
**event emitter**.
Event Emitters
==============
Clients, requests, and any other class that implements the
``GuzzleHttp\Event\HasEmitterInterface`` interface have a
``GuzzleHttp\Event\Emitter`` object. You can add event *listeners* and
event *subscribers* to an event *emitter*.
emitter
An object that implements ``GuzzleHttp\Event\EmitterInterface``. This
object emits named events to event listeners. You may register event
listeners on subscribers on an emitter.
event listeners
Callable functions that are registered on an event emitter for specific
events. Event listeners are registered on an emitter with a *priority*
setting. If no priority is provided, ``0`` is used by default.
event subscribers
Classes that tell an event emitter what methods to listen to and what
functions on the class to invoke when the event is triggered. Event
subscribers subscribe event listeners to an event emitter. They should be
used when creating more complex event based logic in applications (i.e.,
cookie handling is implemented using an event subscriber because it's
easier to share a subscriber than an anonymous function and because
handling cookies is a complex process).
priority
Describes the order in which event listeners are invoked when an event is
emitted. The higher a priority value, the earlier the event listener will
be invoked (a higher priority means the listener is more important). If
no priority is provided, the priority is assumed to be ``0``.
When specifying an event priority, you can pass ``"first"`` or ``"last"`` to
dynamically specify the priority based on the current event priorities
associated with the given event name in the emitter. Use ``"first"`` to set
the priority to the current highest priority plus one. Use ``"last"`` to
set the priority to the current lowest event priority minus one. It is
important to remember that these dynamic priorities are calculated only at
the point of insertion into the emitter and they are not rearranged after
subsequent listeners are added to an emitter.
propagation
Describes whether or not other event listeners are triggered. Event
emitters will trigger every event listener registered to a specific event
in priority order until all of the listeners have been triggered **or**
until the propagation of an event is stopped.
Getting an EventEmitter
-----------------------
You can get the event emitter of ``GuzzleHttp\Event\HasEmitterInterface``
object using the the ``getEmitter()`` method. Here's an example of getting a
client object's event emitter.
.. code-block:: php
$client = new GuzzleHttp\Client();
$emitter = $client->getEmitter();
.. note::
You'll notice that the event emitter used in Guzzle is very similar to the
`Symfony2 EventDispatcher component <https://github.com/symfony/symfony/tree/master/src/Symfony/Component/EventDispatcher>`_.
This is because the Guzzle event system is based on the Symfony2 event
system with several changes. Guzzle uses its own event emitter to improve
performance, isolate Guzzle from changes to the Symfony, and provide a few
improvements that make it easier to use for an HTTP client (e.g., the
addition of the ``once()`` method).
Adding Event Listeners
----------------------
After you have the emitter, you can register event listeners that listen to
specific events using the ``on()`` method. When registering an event listener,
you must tell the emitter what event to listen to (e.g., "before", "after",
"progress", "complete", "error", etc.), what callable to invoke when the
event is triggered, and optionally provide a priority.
.. code-block:: php
use GuzzleHttp\Event\BeforeEvent;
$emitter->on('before', function (BeforeEvent $event) {
echo $event->getRequest();
});
When a listener is triggered, it is passed an event that implements the
``GuzzleHttp\Event\EventInterface`` interface, the name of the event, and the
event emitter itself. The above example could more verbosely be written as
follows:
.. code-block:: php
use GuzzleHttp\Event\BeforeEvent;
$emitter->on('before', function (
BeforeEvent $event,
$name,
EmitterInterface $emitter
) {
echo $event->getRequest();
});
You can add an event listener that automatically removes itself after it is
triggered using the ``once()`` method of an event emitter.
.. code-block:: php
$client = new GuzzleHttp\Client();
$client->getEmitter()->once('before', function () {
echo 'This will only happen once... per request!';
});
Event Propagation
-----------------
Event listeners can prevent other event listeners from being triggered by
stopping an event's propagation.
Stopping event propagation can be useful, for example, if an event listener has
changed the state of the subject to such an extent that allowing subsequent
event listeners to be triggered could place the subject in an inconsistent
state. This technique is used in Guzzle extensively when intercepting error
events with responses.
You can stop the propagation of an event using the ``stopPropagation()`` method
of a ``GuzzleHttp\Event\EventInterface`` object:
.. code-block:: php
use GuzzleHttp\Event\ErrorEvent;
$emitter->on('error', function (ErrorEvent $event) {
$event->stopPropagation();
});
After stopping the propagation of an event, any subsequent event listeners that
have not yet been triggered will not be triggered. You can check to see if the
propagation of an event was stopped using the ``isPropagationStopped()`` method
of the event.
.. code-block:: php
$client = new GuzzleHttp\Client();
$emitter = $client->getEmitter();
// Note: assume that the $errorEvent was created
if ($emitter->emit('error', $errorEvent)->isPropagationStopped()) {
echo 'It was stopped!';
}
.. hint::
When emitting events, the event that was emitted is returned from the
emitter. This allows you to easily chain calls as shown in the above
example.
Event Subscribers
-----------------
Event subscribers are classes that implement the
``GuzzleHttp\Event\SubscriberInterface`` object. They are used to register
one or more event listeners to methods of the class. Event subscribers tell
event emitters exactly which events to listen to and what method to invoke on
the class when the event is triggered by called the ``getEvents()`` method of
a subscriber.
The following example registers event listeners to the ``before`` and
``complete`` event of a request. When the ``before`` event is emitted, the
``onBefore`` instance method of the subscriber is invoked. When the
``complete`` event is emitted, the ``onComplete`` event of the subscriber is
invoked. Each array value in the ``getEvents()`` return value MUST
contain the name of the method to invoke and can optionally contain the
priority of the listener (as shown in the ``before`` listener in the example).
.. code-block:: php
use GuzzleHttp\Event\EmitterInterface;
use GuzzleHttp\Event\SubscriberInterface;
use GuzzleHttp\Event\BeforeEvent;
use GuzzleHttp\Event\CompleteEvent;
class SimpleSubscriber implements SubscriberInterface
{
public function getEvents()
{
return [
// Provide name and optional priority
'before' => ['onBefore', 100],
'complete' => ['onComplete'],
// You can pass a list of listeners with different priorities
'error' => [['beforeError', 'first'], ['afterError', 'last']]
];
}
public function onBefore(BeforeEvent $event, $name)
{
echo 'Before!';
}
public function onComplete(CompleteEvent $event, $name)
{
echo 'Complete!';
}
}
To register the listeners the subscriber needs to be attached to the emitter:
.. code-block:: php
$client = new GuzzleHttp\Client();
$emitter = $client->getEmitter();
$subscriber = new SimpleSubscriber();
$emitter->attach($subscriber);
//to remove the listeners
$emitter->detach($subscriber);
.. note::
You can specify event priorities using integers or ``"first"`` and
``"last"`` to dynamically determine the priority.
Event Priorities
================
When adding event listeners or subscribers, you can provide an optional event
priority. This priority is used to determine how early or late a listener is
triggered. Specifying the correct priority is an important aspect of ensuring
a listener behaves as expected. For example, if you wanted to ensure that
cookies associated with a redirect were added to a cookie jar, you'd need to
make sure that the listener that collects the cookies is triggered before the
listener that performs the redirect.
In order to help make the process of determining the correct event priority of
a listener easier, Guzzle provides several pre-determined named event
priorities. These priorities are exposed as constants on the
``GuzzleHttp\Event\RequestEvents`` object.
last
Use ``"last"`` as an event priority to set the priority to the current
lowest event priority minus one.
first
Use ``"first"`` as an event priority to set the priority to the current
highest priority plus one.
``GuzzleHttp\Event\RequestEvents::EARLY``
Used when you want a listener to be triggered as early as possible in the
event chain.
``GuzzleHttp\Event\RequestEvents::LATE``
Used when you want a listener to be to be triggered as late as possible in
the event chain.
``GuzzleHttp\Event\RequestEvents::PREPARE_REQUEST``
Used when you want a listener to be trigger while a request is being
prepared during the ``before`` event. This event priority is used by the
``GuzzleHttp\Subscriber\Prepare`` event subscriber which is responsible for
guessing a Content-Type, Content-Length, and Expect header of a request.
You should subscribe after this event is triggered if you want to ensure
that this subscriber has already been triggered.
``GuzzleHttp\Event\RequestEvents::SIGN_REQUEST``
Used when you want a listener to be triggered when a request is about to be
signed. Any listener triggered at this point should expect that the request
object will no longer be mutated. If you are implementing a custom
signature subscriber, then you should use this event priority to sign
requests.
``GuzzleHttp\Event\RequestEvents::VERIFY_RESPONSE``
Used when you want a listener to be triggered when a response is being
validated during the ``complete`` event. The
``GuzzleHttp\Subscriber\HttpError`` event subscriber uses this event
priority to check if an exception should be thrown due to a 4xx or 5xx
level response status code. If you are doing any kind of verification of a
response during the complete event, it should happen at this priority.
``GuzzleHttp\Event\RequestEvents::REDIRECT_RESPONSE``
Used when you want a listener to be triggered when a response is being
redirected during the ``complete`` event. The
``GuzzleHttp\Subscriber\Redirect`` event subscriber uses this event
priority when performing redirects.
You can use the above event priorities as a guideline for determining the
priority of you event listeners. You can use these constants and add to or
subtract from them to ensure that a listener happens before or after the named
priority.
.. note::
"first" and "last" priorities are not adjusted after they added to an
emitter. For example, if you add a listener with a priority of "first",
you can still add subsequent listeners with a higher priority which would
be triggered before the listener added with a priority of "first".
Working With Request Events
===========================
Requests emit lifecycle events when they are transferred.
.. important::
Excluding the ``end`` event, request lifecycle events may be triggered
multiple times due to redirects, retries, or reusing a request multiple
times. Use the ``once()`` method want the event to be triggered once. You
can also remove an event listener from an emitter by using the emitter which
is provided to the listener.
.. _before_event:
before
------
The ``before`` event is emitted before a request is sent. The event emitted is
a ``GuzzleHttp\Event\BeforeEvent``.
.. code-block:: php
use GuzzleHttp\Client;
use GuzzleHttp\Event\EmitterInterface;
use GuzzleHttp\Event\BeforeEvent;
$client = new Client(['base_url' => 'http://httpbin.org']);
$request = $client->createRequest('GET', '/');
$request->getEmitter()->on(
'before',
function (BeforeEvent $e, $name, EmitterInterface $emitter) {
echo $name . "\n";
// "before"
echo $e->getRequest()->getMethod() . "\n";
// "GET" / "POST" / "PUT" / etc.
echo get_class($e->getClient());
// "GuzzleHttp\Client"
}
);
You can intercept a request with a response before the request is sent over the
wire. The ``intercept()`` method of the ``BeforeEvent`` accepts a
``GuzzleHttp\Message\ResponseInterface``. Intercepting the event will prevent
the request from being sent over the wire and stops the propagation of the
``before`` event, preventing subsequent event listeners from being invoked.
.. code-block:: php
use GuzzleHttp\Client;
use GuzzleHttp\Event\BeforeEvent;
use GuzzleHttp\Message\Response;
$client = new Client(['base_url' => 'http://httpbin.org']);
$request = $client->createRequest('GET', '/status/500');
$request->getEmitter()->on('before', function (BeforeEvent $e) {
$response = new Response(200);
$e->intercept($response);
});
$response = $client->send($request);
echo $response->getStatusCode();
// 200
.. attention::
Any exception encountered while executing the ``before`` event will trigger
the ``error`` event of a request.
.. _complete_event:
complete
--------
The ``complete`` event is emitted after a transaction completes and an entire
response has been received. The event is a ``GuzzleHttp\Event\CompleteEvent``.
You can intercept the ``complete`` event with a different response if needed
using the ``intercept()`` method of the event. This can be useful, for example,
for changing the response for caching.
.. code-block:: php
use GuzzleHttp\Client;
use GuzzleHttp\Event\CompleteEvent;
use GuzzleHttp\Message\Response;
$client = new Client(['base_url' => 'http://httpbin.org']);
$request = $client->createRequest('GET', '/status/302');
$cachedResponse = new Response(200);
$request->getEmitter()->on(
'complete',
function (CompleteEvent $e) use ($cachedResponse) {
if ($e->getResponse()->getStatusCode() == 302) {
// Intercept the original transaction with the new response
$e->intercept($cachedResponse);
}
}
);
$response = $client->send($request);
echo $response->getStatusCode();
// 200
.. attention::
Any ``GuzzleHttp\Exception\RequestException`` encountered while executing
the ``complete`` event will trigger the ``error`` event of a request.
.. _error_event:
error
-----
The ``error`` event is emitted when a request fails (whether it's from a
networking error or an HTTP protocol error). The event emitted is a
``GuzzleHttp\Event\ErrorEvent``.
This event is useful for retrying failed requests. Here's an example of
retrying failed basic auth requests by re-sending the original request with
a username and password.
.. code-block:: php
use GuzzleHttp\Client;
use GuzzleHttp\Event\ErrorEvent;
$client = new Client(['base_url' => 'http://httpbin.org']);
$request = $client->createRequest('GET', '/basic-auth/foo/bar');
$request->getEmitter()->on('error', function (ErrorEvent $e) {
if ($e->getResponse()->getStatusCode() == 401) {
// Add authentication stuff as needed and retry the request
$e->getRequest()->setHeader('Authorization', 'Basic ' . base64_encode('foo:bar'));
// Get the client of the event and retry the request
$newResponse = $e->getClient()->send($e->getRequest());
// Intercept the original transaction with the new response
$e->intercept($newResponse);
}
});
.. attention::
If an ``error`` event is intercepted with a response, then the ``complete``
event of a request is triggered. If the ``complete`` event fails, then the
``error`` event is triggered once again.
.. _progress_event:
progress
--------
The ``progress`` event is emitted when data is uploaded or downloaded. The
event emitted is a ``GuzzleHttp\Event\ProgressEvent``.
You can access the emitted progress values using the corresponding public
properties of the event object:
- ``$downloadSize``: The number of bytes that will be downloaded (if known)
- ``$downloaded``: The number of bytes that have been downloaded
- ``$uploadSize``: The number of bytes that will be uploaded (if known)
- ``$uploaded``: The number of bytes that have been uploaded
This event cannot be intercepted.
.. code-block:: php
use GuzzleHttp\Client;
use GuzzleHttp\Event\ProgressEvent;
$client = new Client(['base_url' => 'http://httpbin.org']);
$request = $client->createRequest('PUT', '/put', [
'body' => str_repeat('.', 100000)
]);
$request->getEmitter()->on('progress', function (ProgressEvent $e) {
echo 'Downloaded ' . $e->downloaded . ' of ' . $e->downloadSize . ' '
. 'Uploaded ' . $e->uploaded . ' of ' . $e->uploadSize . "\r";
});
$client->send($request);
echo "\n";
.. _end_event:
end
---
The ``end`` event is a terminal event, emitted once per request, that provides
access to the response that was received or the exception that was encountered.
The event emitted is a ``GuzzleHttp\Event\EndEvent``.
This event can be intercepted, but keep in mind that the ``complete`` event
will not fire after intercepting this event.
.. code-block:: php
use GuzzleHttp\Client;
use GuzzleHttp\Event\EndEvent;
$client = new Client(['base_url' => 'http://httpbin.org']);
$request = $client->createRequest('PUT', '/put', [
'body' => str_repeat('.', 100000)
]);
$request->getEmitter()->on('end', function (EndEvent $e) {
if ($e->getException()) {
echo 'Error: ' . $e->getException()->getMessage();
} else {
echo 'Response: ' . $e->getResponse();
}
});
$client->send($request);
echo "\n";

View file

@ -1,199 +0,0 @@
===
FAQ
===
Why should I use Guzzle?
========================
Guzzle makes it easy to send HTTP requests and super simple to integrate with
web services. Guzzle manages things like persistent connections, represents
query strings as collections, makes it simple to send streaming POST requests
with fields and files, and abstracts away the underlying HTTP transport layer.
By providing an object oriented interface for HTTP clients, requests, responses,
headers, and message bodies, Guzzle makes it so that you no longer need to fool
around with cURL options, stream contexts, or sockets.
**Asynchronous and Synchronous Requests**
Guzzle allows you to send both asynchronous and synchronous requests using the
same interface and no direct dependency on an event loop. This flexibility
allows Guzzle to send an HTTP request using the most appropriate HTTP handler
based on the request being sent. For example, when sending synchronous
requests, Guzzle will by default send requests using cURL easy handles to
ensure you're using the fastest possible method for serially transferring HTTP
requests. When sending asynchronous requests, Guzzle might use cURL's multi
interface or any other asynchronous handler you configure. When you request
streaming data, Guzzle will by default use PHP's stream wrapper.
**Streams**
Request and response message bodies use :doc:`Guzzle Streams <streams>`,
allowing you to stream data without needing to load it all into memory.
Guzzle's stream layer provides a large suite of functionality:
- You can modify streams at runtime using custom or a number of
pre-made decorators.
- You can emit progress events as data is read from a stream.
- You can validate the integrity of a stream using a rolling hash as data is
read from a stream.
**Event System and Plugins**
Guzzle's event system allows you to completely modify the behavior of a client
or request at runtime to cater them for any API. You can send a request with a
client, and the client can do things like automatically retry your request if
it fails, automatically redirect, log HTTP messages that are sent over the
wire, emit progress events as data is uploaded and downloaded, sign requests
using OAuth 1.0, verify the integrity of messages before and after they are
sent over the wire, and anything else you might need.
**Testable**
Another important aspect of Guzzle is that it's really
:doc:`easy to test clients <testing>`. You can mock HTTP responses and when
testing an handler implementation, Guzzle provides a mock node.js web server.
**Ecosystem**
Guzzle has a large `ecosystem of plugins <http://guzzle.readthedocs.org/en/latest/index.html#http-components>`_,
including `service descriptions <https://github.com/guzzle/guzzle-services>`_
which allows you to abstract web services using service descriptions. These
service descriptions define how to serialize an HTTP request and how to parse
an HTTP response into a more meaningful model object.
- `Guzzle Command <https://github.com/guzzle/command>`_: Provides the building
blocks for service description abstraction.
- `Guzzle Services <https://github.com/guzzle/guzzle-services>`_: Provides an
implementation of "Guzzle Command" that utilizes Guzzle's service description
format.
Does Guzzle require cURL?
=========================
No. Guzzle can use any HTTP handler to send requests. This means that Guzzle
can be used with cURL, PHP's stream wrapper, sockets, and non-blocking libraries
like `React <http://reactphp.org/>`_. You just need to configure a
`RingPHP <http://guzzle-ring.readthedocs.org/en/latest/>`_ handler to use a
different method of sending requests.
.. note::
Guzzle has historically only utilized cURL to send HTTP requests. cURL is
an amazing HTTP client (arguably the best), and Guzzle will continue to use
it by default when it is available. It is rare, but some developers don't
have cURL installed on their systems or run into version specific issues.
By allowing swappable HTTP handlers, Guzzle is now much more customizable
and able to adapt to fit the needs of more developers.
Can Guzzle send asynchronous requests?
======================================
Yes. Pass the ``future`` true request option to a request to send it
asynchronously. Guzzle will then return a ``GuzzleHttp\Message\FutureResponse``
object that can be used synchronously by accessing the response object like a
normal response, and it can be used asynchronously using a promise that is
notified when the response is resolved with a real response or rejected with an
exception.
.. code-block:: php
$request = $client->createRequest('GET', ['future' => true]);
$client->send($request)->then(function ($response) {
echo 'Got a response! ' . $response;
});
You can force an asynchronous response to complete using the ``wait()`` method
of a response.
.. code-block:: php
$request = $client->createRequest('GET', ['future' => true]);
$futureResponse = $client->send($request);
$futureResponse->wait();
How can I add custom cURL options?
==================================
cURL offer a huge number of `customizable options <http://us1.php.net/curl_setopt>`_.
While Guzzle normalizes many of these options across different handlers, there
are times when you need to set custom cURL options. This can be accomplished
by passing an associative array of cURL settings in the **curl** key of the
**config** request option.
For example, let's say you need to customize the outgoing network interface
used with a client.
.. code-block:: php
$client->get('/', [
'config' => [
'curl' => [
CURLOPT_INTERFACE => 'xxx.xxx.xxx.xxx'
]
]
]);
How can I add custom stream context options?
============================================
You can pass custom `stream context options <http://www.php.net/manual/en/context.php>`_
using the **stream_context** key of the **config** request option. The
**stream_context** array is an associative array where each key is a PHP
transport, and each value is an associative array of transport options.
For example, let's say you need to customize the outgoing network interface
used with a client and allow self-signed certificates.
.. code-block:: php
$client->get('/', [
'stream' => true,
'config' => [
'stream_context' => [
'ssl' => [
'allow_self_signed' => true
],
'socket' => [
'bindto' => 'xxx.xxx.xxx.xxx'
]
]
]
]);
Why am I getting an SSL verification error?
===========================================
You need to specify the path on disk to the CA bundle used by Guzzle for
verifying the peer certificate. See :ref:`verify-option`.
What is this Maximum function nesting error?
============================================
Maximum function nesting level of '100' reached, aborting
You could run into this error if you have the XDebug extension installed and
you execute a lot of requests in callbacks. This error message comes
specifically from the XDebug extension. PHP itself does not have a function
nesting limit. Change this setting in your php.ini to increase the limit::
xdebug.max_nesting_level = 1000
Why am I getting a 417 error response?
======================================
This can occur for a number of reasons, but if you are sending PUT, POST, or
PATCH requests with an ``Expect: 100-Continue`` header, a server that does not
support this header will return a 417 response. You can work around this by
setting the ``expect`` request option to ``false``:
.. code-block:: php
$client = new GuzzleHttp\Client();
// Disable the expect header on a single request
$response = $client->put('/', [], 'the body', [
'expect' => false
]);
// Disable the expect header on all client requests
$client->setDefaultOption('expect', false)

View file

@ -1,43 +0,0 @@
================
RingPHP Handlers
================
Guzzle uses RingPHP handlers to send HTTP requests over the wire.
RingPHP provides a low-level library that can be used to "glue" Guzzle with
any transport method you choose. By default, Guzzle utilizes cURL and PHP's
stream wrappers to send HTTP requests.
RingPHP handlers makes it extremely simple to integrate Guzzle with any
HTTP transport. For example, you could quite easily bridge Guzzle and React
to use Guzzle in React's event loop.
Using a handler
---------------
You can change the handler used by a client using the ``handler`` option in the
``GuzzleHttp\Client`` constructor.
.. code-block:: php
use GuzzleHttp\Client;
use GuzzleHttp\Ring\Client\MockHandler;
// Create a mock handler that always returns a 200 response.
$handler = new MockHandler(['status' => 200]);
// Configure to client to use the mock handler.
$client = new Client(['handler' => $handler]);
At its core, handlers are simply PHP callables that accept a request array
and return a ``GuzzleHttp\Ring\Future\FutureArrayInterface``. This future array
can be used just like a normal PHP array, causing it to block, or you can use
the promise interface using the ``then()`` method of the future. Guzzle hooks
up to the RingPHP project using a very simple bridge class
(``GuzzleHttp\RingBridge``).
Creating a handler
------------------
See the `RingPHP <http://ringphp.readthedocs.org>`_ project
documentation for more information on creating custom handlers that can be
used with Guzzle clients.

View file

@ -1,483 +0,0 @@
=============================
Request and Response Messages
=============================
Guzzle is an HTTP client that sends HTTP requests to a server and receives HTTP
responses. Both requests and responses are referred to as messages.
Headers
=======
Both request and response messages contain HTTP headers.
Complex Headers
---------------
Some headers contain additional key value pair information. For example, Link
headers contain a link and several key value pairs:
::
<http://foo.com>; rel="thing"; type="image/jpeg"
Guzzle provides a convenience feature that can be used to parse these types of
headers:
.. code-block:: php
use GuzzleHttp\Message\Request;
$request = new Request('GET', '/', [
'Link' => '<http:/.../front.jpeg>; rel="front"; type="image/jpeg"'
]);
$parsed = Request::parseHeader($request, 'Link');
var_export($parsed);
Will output:
.. code-block:: php
array (
0 =>
array (
0 => '<http:/.../front.jpeg>',
'rel' => 'front',
'type' => 'image/jpeg',
),
)
The result contains a hash of key value pairs. Header values that have no key
(i.e., the link) are indexed numerically while headers parts that form a key
value pair are added as a key value pair.
See :ref:`headers` for information on how the headers of a request and response
can be accessed and modified.
Body
====
Both request and response messages can contain a body.
You can check to see if a request or response has a body using the
``getBody()`` method:
.. code-block:: php
$response = GuzzleHttp\get('http://httpbin.org/get');
if ($response->getBody()) {
echo $response->getBody();
// JSON string: { ... }
}
The body used in request and response objects is a
``GuzzleHttp\Stream\StreamInterface``. This stream is used for both uploading
data and downloading data. Guzzle will, by default, store the body of a message
in a stream that uses PHP temp streams. When the size of the body exceeds
2 MB, the stream will automatically switch to storing data on disk rather than
in memory (protecting your application from memory exhaustion).
You can change the body used in a request or response using the ``setBody()``
method:
.. code-block:: php
use GuzzleHttp\Stream\Stream;
$request = $client->createRequest('PUT', 'http://httpbin.org/put');
$request->setBody(Stream::factory('foo'));
The easiest way to create a body for a request is using the static
``GuzzleHttp\Stream\Stream::factory()`` method. This method accepts various
inputs like strings, resources returned from ``fopen()``, and other
``GuzzleHttp\Stream\StreamInterface`` objects.
The body of a request or response can be cast to a string or you can read and
write bytes off of the stream as needed.
.. code-block:: php
use GuzzleHttp\Stream\Stream;
$request = $client->createRequest('PUT', 'http://httpbin.org/put', ['body' => 'testing...']);
echo $request->getBody()->read(4);
// test
echo $request->getBody()->read(4);
// ing.
echo $request->getBody()->read(1024);
// ..
var_export($request->eof());
// true
You can find out more about Guzzle stream objects in :doc:`streams`.
Requests
========
Requests are sent from a client to a server. Requests include the method to
be applied to a resource, the identifier of the resource, and the protocol
version to use.
Clients are used to create request messages. More precisely, clients use
a ``GuzzleHttp\Message\MessageFactoryInterface`` to create request messages.
You create requests with a client using the ``createRequest()`` method.
.. code-block:: php
// Create a request but don't send it immediately
$request = $client->createRequest('GET', 'http://httpbin.org/get');
Request Methods
---------------
When creating a request, you are expected to provide the HTTP method you wish
to perform. You can specify any method you'd like, including a custom method
that might not be part of RFC 7231 (like "MOVE").
.. code-block:: php
// Create a request using a completely custom HTTP method
$request = $client->createRequest('MOVE', 'http://httpbin.org/move', ['exceptions' => false]);
echo $request->getMethod();
// MOVE
$response = $client->send($request);
echo $response->getStatusCode();
// 405
You can create and send a request using methods on a client that map to the
HTTP method you wish to use.
:GET: ``$client->get('http://httpbin.org/get', [/** options **/])``
:POST: ``$client->post('http://httpbin.org/post', [/** options **/])``
:HEAD: ``$client->head('http://httpbin.org/get', [/** options **/])``
:PUT: ``$client->put('http://httpbin.org/put', [/** options **/])``
:DELETE: ``$client->delete('http://httpbin.org/delete', [/** options **/])``
:OPTIONS: ``$client->options('http://httpbin.org/get', [/** options **/])``
:PATCH: ``$client->patch('http://httpbin.org/put', [/** options **/])``
.. code-block:: php
$response = $client->patch('http://httpbin.org/patch', ['body' => 'content']);
Request URI
-----------
The resource you are requesting with an HTTP request is identified by the
path of the request, the query string, and the "Host" header of the request.
When creating a request, you can provide the entire resource URI as a URL.
.. code-block:: php
$response = $client->get('http://httbin.org/get?q=foo');
Using the above code, you will send a request that uses ``httpbin.org`` as
the Host header, sends the request over port 80, uses ``/get`` as the path,
and sends ``?q=foo`` as the query string. All of this is parsed automatically
from the provided URI.
Sometimes you don't know what the entire request will be when it is created.
In these cases, you can modify the request as needed before sending it using
the ``createRequest()`` method of the client and methods on the request that
allow you to change it.
.. code-block:: php
$request = $client->createRequest('GET', 'http://httbin.org');
You can change the path of the request using ``setPath()``:
.. code-block:: php
$request->setPath('/get');
echo $request->getPath();
// /get
echo $request->getUrl();
// http://httpbin.com/get
Scheme
~~~~~~
The `scheme <http://tools.ietf.org/html/rfc3986#section-3.1>`_ of a request
specifies the protocol to use when sending the request. When using Guzzle, the
scheme can be set to "http" or "https".
You can change the scheme of the request using the ``setScheme()`` method:
.. code-block:: php
$request = $client->createRequest('GET', 'http://httbin.org');
$request->setScheme('https');
echo $request->getScheme();
// https
echo $request->getUrl();
// https://httpbin.com/get
Port
~~~~
No port is necessary when using the "http" or "https" schemes, but you can
override the port using ``setPort()``. If you need to modify the port used with
the specified scheme from the default setting, then you must use the
``setPort()`` method.
.. code-block:: php
$request = $client->createRequest('GET', 'http://httbin.org');
$request->setPort(8080);
echo $request->getPort();
// 8080
echo $request->getUrl();
// https://httpbin.com:8080/get
// Set the port back to the default value for the scheme
$request->setPort(443);
echo $request->getUrl();
// https://httpbin.com/get
Query string
~~~~~~~~~~~~
You can get the query string of the request using the ``getQuery()`` method.
This method returns a ``GuzzleHttp\Query`` object. A Query object can be
accessed like a PHP array, iterated in a foreach statement like a PHP array,
and cast to a string.
.. code-block:: php
$request = $client->createRequest('GET', 'http://httbin.org');
$query = $request->getQuery();
$query['foo'] = 'bar';
$query['baz'] = 'bam';
$query['bam'] = ['test' => 'abc'];
echo $request->getQuery();
// foo=bar&baz=bam&bam%5Btest%5D=abc
echo $request->getQuery()['foo'];
// bar
echo $request->getQuery()->get('foo');
// bar
echo $request->getQuery()->get('foo');
// bar
var_export($request->getQuery()['bam']);
// array('test' => 'abc')
foreach ($query as $key => $value) {
var_export($value);
}
echo $request->getUrl();
// https://httpbin.com/get?foo=bar&baz=bam&bam%5Btest%5D=abc
Query Aggregators
^^^^^^^^^^^^^^^^^
Query objects can store scalar values or arrays of values. When an array of
values is added to a query object, the query object uses a query aggregator to
convert the complex structure into a string. Query objects will use
`PHP style query strings <http://www.php.net/http_build_query>`_ when complex
query string parameters are converted to a string. You can customize how
complex query string parameters are aggregated using the ``setAggregator()``
method of a query string object.
.. code-block:: php
$query->setAggregator($query::duplicateAggregator());
In the above example, we've changed the query object to use the
"duplicateAggregator". This aggregator will allow duplicate entries to appear
in a query string rather than appending "[n]" to each value. So if you had a
query string with ``['a' => ['b', 'c']]``, the duplicate aggregator would
convert this to "a=b&a=c" while the default aggregator would convert this to
"a[0]=b&a[1]=c" (with urlencoded brackets).
The ``setAggregator()`` method accepts a ``callable`` which is used to convert
a deeply nested array of query string variables into a flattened array of key
value pairs. The callable accepts an array of query data and returns a
flattened array of key value pairs where each value is an array of strings.
You can use the ``GuzzleHttp\Query::walkQuery()`` static function to easily
create custom query aggregators.
Host
~~~~
You can change the host header of the request in a predictable way using the
``setHost()`` method of a request:
.. code-block:: php
$request->setHost('www.google.com');
echo $request->getHost();
// www.google.com
echo $request->getUrl();
// https://www.google.com/get?foo=bar&baz=bam
.. note::
The Host header can also be changed by modifying the Host header of a
request directly, but modifying the Host header directly could result in
sending a request to a different Host than what is specified in the Host
header (sometimes this is actually the desired behavior).
Resource
~~~~~~~~
You can use the ``getResource()`` method of a request to return the path and
query string of a request in a single string.
.. code-block:: php
$request = $client->createRequest('GET', 'http://httpbin.org/get?baz=bar');
echo $request->getResource();
// /get?baz=bar
Request Config
--------------
Request messages contain a configuration collection that can be used by
event listeners and HTTP handlers to modify how a request behaves or is
transferred over the wire. For example, many of the request options that are
specified when creating a request are actually set as config options that are
only acted upon by handlers and listeners when the request is sent.
You can get access to the request's config object using the ``getConfig()``
method of a request.
.. code-block:: php
$request = $client->createRequest('GET', '/');
$config = $request->getConfig();
The config object is a ``GuzzleHttp\Collection`` object that acts like
an associative array. You can grab values from the collection using array like
access. You can also modify and remove values using array like access.
.. code-block:: php
$config['foo'] = 'bar';
echo $config['foo'];
// bar
var_export(isset($config['foo']));
// true
unset($config['foo']);
var_export(isset($config['foo']));
// false
var_export($config['foo']);
// NULL
HTTP handlers and event listeners can expose additional customization options
through request config settings. For example, in order to specify custom cURL
options to the cURL handler, you need to specify an associative array in the
``curl`` ``config`` request option.
.. code-block:: php
$client->get('/', [
'config' => [
'curl' => [
CURLOPT_HTTPAUTH => CURLAUTH_NTLM,
CURLOPT_USERPWD => 'username:password'
]
]
]);
Consult the HTTP handlers and event listeners you are using to see if they
allow customization through request configuration options.
Event Emitter
-------------
Request objects implement ``GuzzleHttp\Event\HasEmitterInterface``, so they
have a method called ``getEmitter()`` that can be used to get an event emitter
used by the request. Any listener or subscriber attached to a request will only
be triggered for the lifecycle events of a specific request. Conversely, adding
an event listener or subscriber to a client will listen to all lifecycle events
of all requests created by the client.
See :doc:`events` for more information.
Responses
=========
Responses are the HTTP messages a client receives from a server after sending
an HTTP request message.
Start-Line
----------
The start-line of a response contains the protocol and protocol version,
status code, and reason phrase.
.. code-block:: php
$response = GuzzleHttp\get('http://httpbin.org/get');
echo $response->getStatusCode();
// 200
echo $response->getReasonPhrase();
// OK
echo $response->getProtocolVersion();
// 1.1
Body
----
As described earlier, you can get the body of a response using the
``getBody()`` method.
.. code-block:: php
if ($body = $response->getBody()) {
echo $body;
// Cast to a string: { ... }
$body->seek(0);
// Rewind the body
$body->read(1024);
// Read bytes of the body
}
When working with JSON responses, you can use the ``json()`` method of a
response:
.. code-block:: php
$json = $response->json();
.. note::
Guzzle uses the ``json_decode()`` method of PHP and uses arrays rather than
``stdClass`` objects for objects.
You can use the ``xml()`` method when working with XML data.
.. code-block:: php
$xml = $response->xml();
.. note::
Guzzle uses the ``SimpleXMLElement`` objects when converting response
bodies to XML.
Effective URL
-------------
The URL that was ultimately accessed that returned a response can be accessed
using the ``getEffectiveUrl()`` method of a response. This method will return
the URL of a request or the URL of the last redirected URL if any redirects
occurred while transferring a request.
.. code-block:: php
$response = GuzzleHttp\get('http://httpbin.org/get');
echo $response->getEffectiveUrl();
// http://httpbin.org/get
$response = GuzzleHttp\get('http://httpbin.org/redirect-to?url=http://www.google.com');
echo $response->getEffectiveUrl();
// http://www.google.com

View file

@ -1,98 +0,0 @@
.. title:: Guzzle | PHP HTTP client and framework for consuming RESTful web services
======
Guzzle
======
Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and
trivial to integrate with web services.
- Manages things like persistent connections, represents query strings as
collections, simplifies sending streaming POST requests with fields and
files, and abstracts away the underlying HTTP transport layer.
- Can send both synchronous and asynchronous requests using the same interface
without requiring a dependency on a specific event loop.
- Pluggable HTTP handlers allows Guzzle to integrate with any method you choose
for sending HTTP requests over the wire (e.g., cURL, sockets, PHP's stream
wrapper, non-blocking event loops like `React <http://reactphp.org/>`_, etc.).
- Guzzle makes it so that you no longer need to fool around with cURL options,
stream contexts, or sockets.
.. code-block:: php
$client = new GuzzleHttp\Client();
$response = $client->get('http://guzzlephp.org');
$res = $client->get('https://api.github.com/user', ['auth' => ['user', 'pass']]);
echo $res->getStatusCode();
// "200"
echo $res->getHeader('content-type');
// 'application/json; charset=utf8'
echo $res->getBody();
// {"type":"User"...'
var_export($res->json());
// Outputs the JSON decoded data
// Send an asynchronous request.
$req = $client->createRequest('GET', 'http://httpbin.org', ['future' => true]);
$client->send($req)->then(function ($response) {
echo 'I completed! ' . $response;
});
User guide
----------
.. toctree::
:maxdepth: 2
overview
quickstart
clients
http-messages
events
streams
handlers
testing
faq
HTTP Components
---------------
There are a number of optional libraries you can use along with Guzzle's HTTP
layer to add capabilities to the client.
`Log Subscriber <https://github.com/guzzle/log-subscriber>`_
Logs HTTP requests and responses sent over the wire using customizable
log message templates.
`OAuth Subscriber <https://github.com/guzzle/oauth-subscriber>`_
Signs requests using OAuth 1.0.
`Cache Subscriber <https://github.com/guzzle/cache-subscriber>`_
Implements a private transparent proxy cache that caches HTTP responses.
`Retry Subscriber <https://github.com/guzzle/retry-subscriber>`_
Retries failed requests using customizable retry strategies (e.g., retry
based on response status code, cURL error codes, etc.)
`Message Integrity Subscriber <https://github.com/guzzle/message-integrity-subscriber>`_
Verifies the message integrity of HTTP responses using customizable
validators. This plugin can be used, for example, to verify the Content-MD5
headers of responses.
Service Description Commands
----------------------------
You can use the **Guzzle Command** library to encapsulate interaction with a
web service using command objects. Building on top of Guzzle's command
abstraction allows you to easily implement things like service description that
can be used to serialize requests and parse responses using a meta-description
of a web service.
`Guzzle Command <https://github.com/guzzle/command>`_
Provides the foundational elements used to build high-level, command based,
web service clients with Guzzle.
`Guzzle Services <https://github.com/guzzle/guzzle-services>`_
Provides an implementation of the *Guzzle Command* library that uses
Guzzle service descriptions to describe web services, serialize requests,
and parse responses into easy to use model structures.

View file

@ -1,150 +0,0 @@
========
Overview
========
Requirements
============
#. PHP 5.4.0
#. To use the PHP stream handler, ``allow_url_fopen`` must be enabled in your
system's php.ini.
#. To use the cURL handler, you must have a recent version of cURL >= 7.16.2
compiled with OpenSSL and zlib.
.. note::
Guzzle no longer requires cURL in order to send HTTP requests. Guzzle will
use the PHP stream wrapper to send HTTP requests if cURL is not installed.
Alternatively, you can provide your own HTTP handler used to send requests.
.. _installation:
Installation
============
The recommended way to install Guzzle is with `Composer <http://getcomposer.org>`_. Composer is a dependency
management tool for PHP that allows you to declare the dependencies your project needs and installs them into your
project.
.. code-block:: bash
# Install Composer
curl -sS https://getcomposer.org/installer | php
You can add Guzzle as a dependency using the composer.phar CLI:
.. code-block:: bash
php composer.phar require guzzlehttp/guzzle:~5.0
Alternatively, you can specify Guzzle as a dependency in your project's
existing composer.json file:
.. code-block:: js
{
"require": {
"guzzlehttp/guzzle": "~5.0"
}
}
After installing, you need to require Composer's autoloader:
.. code-block:: php
require 'vendor/autoload.php';
You can find out more on how to install Composer, configure autoloading, and
other best-practices for defining dependencies at `getcomposer.org <http://getcomposer.org>`_.
Bleeding edge
-------------
During your development, you can keep up with the latest changes on the master
branch by setting the version requirement for Guzzle to ``~5.0@dev``.
.. code-block:: js
{
"require": {
"guzzlehttp/guzzle": "~5.0@dev"
}
}
License
=======
Licensed using the `MIT license <http://opensource.org/licenses/MIT>`_.
Copyright (c) 2014 Michael Dowling <https://github.com/mtdowling>
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.
Contributing
============
Guidelines
----------
1. Guzzle follows PSR-0, PSR-1, and PSR-2.
2. Guzzle is meant to be lean and fast with very few dependencies.
3. Guzzle has a minimum PHP version requirement of PHP 5.4. Pull requests must
not require a PHP version greater than PHP 5.4.
4. All pull requests must include unit tests to ensure the change works as
expected and to prevent regressions.
Running the tests
-----------------
In order to contribute, you'll need to checkout the source from GitHub and
install Guzzle's dependencies using Composer:
.. code-block:: bash
git clone https://github.com/guzzle/guzzle.git
cd guzzle && curl -s http://getcomposer.org/installer | php && ./composer.phar install --dev
Guzzle is unit tested with PHPUnit. Run the tests using the vendored PHPUnit
binary:
.. code-block:: bash
vendor/bin/phpunit
.. note::
You'll need to install node.js v0.5.0 or newer in order to perform
integration tests on Guzzle's HTTP handlers.
Reporting a security vulnerability
==================================
We want to ensure that Guzzle is a secure HTTP client library for everyone. If
you've discovered a security vulnerability in Guzzle, we appreciate your help
in disclosing it to us in a `responsible manner <http://en.wikipedia.org/wiki/Responsible_disclosure>`_.
Publicly disclosing a vulnerability can put the entire community at risk. If
you've discovered a security concern, please email us at
security@guzzlephp.org. We'll work with you to make sure that we understand the
scope of the issue, and that we fully address your concern. We consider
correspondence sent to security@guzzlephp.org our highest priority, and work to
address any issues that arise as quickly as possible.
After a security vulnerability has been corrected, a security hotfix release will
be deployed as soon as possible.

View file

@ -1,448 +0,0 @@
==========
Quickstart
==========
This page provides a quick introduction to Guzzle and introductory examples.
If you have not already installed, Guzzle, head over to the :ref:`installation`
page.
Make a Request
==============
You can send requests with Guzzle using a ``GuzzleHttp\ClientInterface``
object.
Creating a Client
-----------------
The procedural API is simple but not very testable; it's best left for quick
prototyping. If you want to use Guzzle in a more flexible and testable way,
then you'll need to use a ``GuzzleHttp\ClientInterface`` object.
.. code-block:: php
use GuzzleHttp\Client;
$client = new Client();
$response = $client->get('http://httpbin.org/get');
// You can use the same methods you saw in the procedural API
$response = $client->delete('http://httpbin.org/delete');
$response = $client->head('http://httpbin.org/get');
$response = $client->options('http://httpbin.org/get');
$response = $client->patch('http://httpbin.org/patch');
$response = $client->post('http://httpbin.org/post');
$response = $client->put('http://httpbin.org/put');
You can create a request with a client and then send the request with the
client when you're ready.
.. code-block:: php
$request = $client->createRequest('GET', 'http://www.foo.com');
$response = $client->send($request);
Client objects provide a great deal of flexibility in how request are
transferred including default request options, subscribers that are attached
to each request, and a base URL that allows you to send requests with relative
URLs. You can find out all about clients in the :doc:`clients` page of the
documentation.
Using Responses
===============
In the previous examples, we retrieved a ``$response`` variable. This value is
actually a ``GuzzleHttp\Message\ResponseInterface`` object and contains lots
of helpful information.
You can get the status code and reason phrase of the response.
.. code-block:: php
$code = $response->getStatusCode();
// 200
$reason = $response->getReasonPhrase();
// OK
By providing the ``future`` request option to a request, you can send requests
asynchronously using the promise interface of a future response.
.. code-block:: php
$client->get('http://httpbin.org', ['future' => true])
->then(function ($response) {
echo $response->getStatusCode();
});
Response Body
-------------
The body of a response can be retrieved and cast to a string.
.. code-block:: php
$body = $response->getBody();
echo $body;
// { "some_json_data" ...}
You can also read read bytes from body of a response like a stream.
.. code-block:: php
$body = $response->getBody();
while (!$body->eof()) {
echo $body->read(1024);
}
JSON Responses
~~~~~~~~~~~~~~
You can more easily work with JSON responses using the ``json()`` method of a
response.
.. code-block:: php
$response = $client->get('http://httpbin.org/get');
$json = $response->json();
var_dump($json[0]['origin']);
Guzzle internally uses PHP's ``json_decode()`` function to parse responses. If
Guzzle is unable to parse the JSON response body, then a
``GuzzleHttp\Exception\ParseException`` is thrown.
XML Responses
~~~~~~~~~~~~~
You can use a response's ``xml()`` method to more easily work with responses
that contain XML data.
.. code-block:: php
$response = $client->get('https://github.com/mtdowling.atom');
$xml = $response->xml();
echo $xml->id;
// tag:github.com,2008:/mtdowling
Guzzle internally uses a ``SimpleXMLElement`` object to parse responses. If
Guzzle is unable to parse the XML response body, then a
``GuzzleHttp\Exception\ParseException`` is thrown.
Query String Parameters
=======================
Sending query string parameters with a request is easy. You can set query
string parameters in the request's URL.
.. code-block:: php
$response = $client->get('http://httpbin.org?foo=bar');
You can also specify the query string parameters using the ``query`` request
option.
.. code-block:: php
$client->get('http://httpbin.org', [
'query' => ['foo' => 'bar']
]);
And finally, you can build up the query string of a request as needed by
calling the ``getQuery()`` method of a request and modifying the request's
``GuzzleHttp\Query`` object as needed.
.. code-block:: php
$request = $client->createRequest('GET', 'http://httpbin.org');
$query = $request->getQuery();
$query->set('foo', 'bar');
// You can use the query string object like an array
$query['baz'] = 'bam';
// The query object can be cast to a string
echo $query;
// foo=bar&baz=bam
// Setting a value to false or null will cause the "=" sign to be omitted
$query['empty'] = null;
echo $query;
// foo=bar&baz=bam&empty
// Use an empty string to include the "=" sign with an empty value
$query['empty'] = '';
echo $query;
// foo=bar&baz=bam&empty=
.. _headers:
Request and Response Headers
----------------------------
You can specify request headers when sending or creating requests with a
client. In the following example, we send the ``X-Foo-Header`` with a value of
``value`` by setting the ``headers`` request option.
.. code-block:: php
$response = $client->get('http://httpbin.org/get', [
'headers' => ['X-Foo-Header' => 'value']
]);
You can view the headers of a response using header specific methods of a
response class. Headers work exactly the same way for request and response
object.
You can retrieve a header from a request or response using the ``getHeader()``
method of the object. This method is case-insensitive and by default will
return a string containing the header field value.
.. code-block:: php
$response = $client->get('http://www.yahoo.com');
$length = $response->getHeader('Content-Length');
Header fields that contain multiple values can be retrieved as a string or as
an array. Retrieving the field values as a string will naively concatenate all
of the header values together with a comma. Because not all header fields
should be represented this way (e.g., ``Set-Cookie``), you can pass an optional
flag to the ``getHeader()`` method to retrieve the header values as an array.
.. code-block:: php
$values = $response->getHeader('Set-Cookie', true);
foreach ($values as $value) {
echo $value;
}
You can test if a request or response has a specific header using the
``hasHeader()`` method. This method accepts a case-insensitive string and
returns true if the header is present or false if it is not.
You can retrieve all of the headers of a message using the ``getHeaders()``
method of a request or response. The return value is an associative array where
the keys represent the header name as it will be sent over the wire, and each
value is an array of strings associated with the header.
.. code-block:: php
$headers = $response->getHeaders();
foreach ($message->getHeaders() as $name => $values) {
echo $name . ": " . implode(", ", $values);
}
Modifying headers
-----------------
The headers of a message can be modified using the ``setHeader()``,
``addHeader()``, ``setHeaders()``, and ``removeHeader()`` methods of a request
or response object.
.. code-block:: php
$request = $client->createRequest('GET', 'http://httpbin.org/get');
// Set a single value for a header
$request->setHeader('User-Agent', 'Testing!');
// Set multiple values for a header in one call
$request->setHeader('X-Foo', ['Baz', 'Bar']);
// Add a header to the message
$request->addHeader('X-Foo', 'Bam');
echo $request->getHeader('X-Foo');
// Baz, Bar, Bam
// Remove a specific header using a case-insensitive name
$request->removeHeader('x-foo');
echo $request->getHeader('X-Foo');
// Echoes an empty string: ''
Uploading Data
==============
Guzzle provides several methods of uploading data.
You can send requests that contain a stream of data by passing a string,
resource returned from ``fopen``, or a ``GuzzleHttp\Stream\StreamInterface``
object to the ``body`` request option.
.. code-block:: php
$r = $client->post('http://httpbin.org/post', ['body' => 'raw data']);
You can easily upload JSON data using the ``json`` request option.
.. code-block:: php
$r = $client->put('http://httpbin.org/put', ['json' => ['foo' => 'bar']]);
POST Requests
-------------
In addition to specifying the raw data of a request using the ``body`` request
option, Guzzle provides helpful abstractions over sending POST data.
Sending POST Fields
~~~~~~~~~~~~~~~~~~~
Sending ``application/x-www-form-urlencoded`` POST requests requires that you
specify the body of a POST request as an array.
.. code-block:: php
$response = $client->post('http://httpbin.org/post', [
'body' => [
'field_name' => 'abc',
'other_field' => '123'
]
]);
You can also build up POST requests before sending them.
.. code-block:: php
$request = $client->createRequest('POST', 'http://httpbin.org/post');
$postBody = $request->getBody();
// $postBody is an instance of GuzzleHttp\Post\PostBodyInterface
$postBody->setField('foo', 'bar');
echo $postBody->getField('foo');
// 'bar'
echo json_encode($postBody->getFields());
// {"foo": "bar"}
// Send the POST request
$response = $client->send($request);
Sending POST Files
~~~~~~~~~~~~~~~~~~
Sending ``multipart/form-data`` POST requests (POST requests that contain
files) is the same as sending ``application/x-www-form-urlencoded``, except
some of the array values of the POST fields map to PHP ``fopen`` resources, or
``GuzzleHttp\Stream\StreamInterface``, or
``GuzzleHttp\Post\PostFileInterface`` objects.
.. code-block:: php
use GuzzleHttp\Post\PostFile;
$response = $client->post('http://httpbin.org/post', [
'body' => [
'field_name' => 'abc',
'file_filed' => fopen('/path/to/file', 'r'),
'other_file' => new PostFile('other_file', 'this is the content')
]
]);
Just like when sending POST fields, you can also build up POST requests with
files before sending them.
.. code-block:: php
use GuzzleHttp\Post\PostFile;
$request = $client->createRequest('POST', 'http://httpbin.org/post');
$postBody = $request->getBody();
$postBody->setField('foo', 'bar');
$postBody->addFile(new PostFile('test', fopen('/path/to/file', 'r')));
$response = $client->send($request);
Cookies
=======
Guzzle can maintain a cookie session for you if instructed using the
``cookies`` request option.
- Set to ``true`` to use a shared cookie session associated with the client.
- Pass an associative array containing cookies to send in the request and start
a new cookie session.
- Set to a ``GuzzleHttp\Subscriber\CookieJar\CookieJarInterface`` object to use
an existing cookie jar.
Redirects
=========
Guzzle will automatically follow redirects unless you tell it not to. You can
customize the redirect behavior using the ``allow_redirects`` request option.
- Set to true to enable normal redirects with a maximum number of 5 redirects.
This is the default setting.
- Set to false to disable redirects.
- Pass an associative array containing the 'max' key to specify the maximum
number of redirects and optionally provide a 'strict' key value to specify
whether or not to use strict RFC compliant redirects (meaning redirect POST
requests with POST requests vs. doing what most browsers do which is
redirect POST requests with GET requests).
.. code-block:: php
$response = $client->get('http://github.com');
echo $response->getStatusCode();
// 200
echo $response->getEffectiveUrl();
// 'https://github.com/'
The following example shows that redirects can be disabled.
.. code-block:: php
$response = $client->get('http://github.com', ['allow_redirects' => false]);
echo $response->getStatusCode();
// 301
echo $response->getEffectiveUrl();
// 'http://github.com/'
Exceptions
==========
Guzzle throws exceptions for errors that occur during a transfer.
- In the event of a networking error (connection timeout, DNS errors, etc.),
a ``GuzzleHttp\Exception\RequestException`` is thrown. This exception
extends from ``GuzzleHttp\Exception\TransferException``. Catching this
exception will catch any exception that can be thrown while transferring
(non-parallel) requests.
.. code-block:: php
use GuzzleHttp\Exception\RequestException;
try {
$client->get('https://github.com/_abc_123_404');
} catch (RequestException $e) {
echo $e->getRequest();
if ($e->hasResponse()) {
echo $e->getResponse();
}
}
- A ``GuzzleHttp\Exception\ClientException`` is thrown for 400
level errors if the ``exceptions`` request option is set to true. This
exception extends from ``GuzzleHttp\Exception\BadResponseException`` and
``GuzzleHttp\Exception\BadResponseException`` extends from
``GuzzleHttp\Exception\RequestException``.
.. code-block:: php
use GuzzleHttp\Exception\ClientException;
try {
$client->get('https://github.com/_abc_123_404');
} catch (ClientException $e) {
echo $e->getRequest();
echo $e->getResponse();
}
- A ``GuzzleHttp\Exception\ServerException`` is thrown for 500 level
errors if the ``exceptions`` request option is set to true. This
exception extends from ``GuzzleHttp\Exception\BadResponseException``.
- A ``GuzzleHttp\Exception\TooManyRedirectsException`` is thrown when too
many redirects are followed. This exception extends from ``GuzzleHttp\Exception\RequestException``.
All of the above exceptions extend from
``GuzzleHttp\Exception\TransferException``.

View file

@ -1,2 +0,0 @@
Sphinx>=1.2b1
guzzle_sphinx_theme>=0.6.0

View file

@ -1,213 +0,0 @@
=======
Streams
=======
Guzzle uses stream objects to represent request and response message bodies.
These stream objects allow you to work with various types of data all using a
common interface.
HTTP messages consist of a start-line, headers, and a body. The body of an HTTP
message can be very small or extremely large. Attempting to represent the body
of a message as a string can easily consume more memory than intended because
the body must be stored completely in memory. Attempting to store the body of a
request or response in memory would preclude the use of that implementation from
being able to work with large message bodies. The StreamInterface is used in
order to hide the implementation details of where a stream of data is read from
or written to.
Guzzle's StreamInterface exposes several methods that enable streams to be read
from, written to, and traversed effectively.
Streams expose their capabilities using three methods: ``isReadable()``,
``isWritable()``, and ``isSeekable()``. These methods can be used by stream
collaborators to determine if a stream is capable of their requirements.
Each stream instance has various capabilities: they can be read-only,
write-only, read-write, allow arbitrary random access (seeking forwards or
backwards to any location), or only allow sequential access (for example in the
case of a socket or pipe).
Creating Streams
================
The best way to create a stream is using the static factory method,
``GuzzleHttp\Stream\Stream::factory()``. This factory accepts strings,
resources returned from ``fopen()``, an object that implements
``__toString()``, and an object that implements
``GuzzleHttp\Stream\StreamInterface``.
.. code-block:: php
use GuzzleHttp\Stream\Stream;
$stream = Stream::factory('string data');
echo $stream;
// string data
echo $stream->read(3);
// str
echo $stream->getContents();
// ing data
var_export($stream->eof());
// true
var_export($stream->tell());
// 11
Metadata
========
Guzzle streams expose stream metadata through the ``getMetadata()`` method.
This method provides the data you would retrieve when calling PHP's
`stream_get_meta_data() function <http://php.net/manual/en/function.stream-get-meta-data.php>`_,
and can optionally expose other custom data.
.. code-block:: php
use GuzzleHttp\Stream\Stream;
$resource = fopen('/path/to/file', 'r');
$stream = Stream::factory($resource);
echo $stream->getMetadata('uri');
// /path/to/file
var_export($stream->isReadable());
// true
var_export($stream->isWritable());
// false
var_export($stream->isSeekable());
// true
Stream Decorators
=================
With the small and focused interface, add custom functionality to streams is
very simple with stream decorators. Guzzle provides several built-in decorators
that provide additional stream functionality.
CachingStream
-------------
The CachingStream is used to allow seeking over previously read bytes on
non-seekable streams. This can be useful when transferring a non-seekable
entity body fails due to needing to rewind the stream (for example, resulting
from a redirect). Data that is read from the remote stream will be buffered in
a PHP temp stream so that previously read bytes are cached first in memory,
then on disk.
.. code-block:: php
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Stream\CachingStream;
$original = Stream::factory(fopen('http://www.google.com', 'r'));
$stream = new CachingStream($original);
$stream->read(1024);
echo $stream->tell();
// 1024
$stream->seek(0);
echo $stream->tell();
// 0
LimitStream
-----------
LimitStream can be used to read a subset or slice of an existing stream object.
This can be useful for breaking a large file into smaller pieces to be sent in
chunks (e.g. Amazon S3's multipart upload API).
.. code-block:: php
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Stream\LimitStream;
$original = Stream::factory(fopen('/tmp/test.txt', 'r+'));
echo $original->getSize();
// >>> 1048576
// Limit the size of the body to 1024 bytes and start reading from byte 2048
$stream = new LimitStream($original, 1024, 2048);
echo $stream->getSize();
// >>> 1024
echo $stream->tell();
// >>> 0
NoSeekStream
------------
NoSeekStream wraps a stream and does not allow seeking.
.. code-block:: php
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Stream\LimitStream;
$original = Stream::factory('foo');
$noSeek = new NoSeekStream($original);
echo $noSeek->read(3);
// foo
var_export($noSeek->isSeekable());
// false
$noSeek->seek(0);
var_export($noSeek->read(3));
// NULL
Creating Custom Decorators
--------------------------
Creating a stream decorator is very easy thanks to the
``GuzzleHttp\Stream\StreamDecoratorTrait``. This trait provides methods that
implement ``GuzzleHttp\Stream\StreamInterface`` by proxying to an underlying
stream. Just ``use`` the ``StreamDecoratorTrait`` and implement your custom
methods.
For example, let's say we wanted to call a specific function each time the last
byte is read from a stream. This could be implemented by overriding the
``read()`` method.
.. code-block:: php
use GuzzleHttp\Stream\StreamDecoratorTrait;
class EofCallbackStream implements StreamInterface
{
use StreamDecoratorTrait;
private $callback;
public function __construct(StreamInterface $stream, callable $callback)
{
$this->stream = $stream;
$this->callback = $callback;
}
public function read($length)
{
$result = $this->stream->read($length);
// Invoke the callback when EOF is hit.
if ($this->eof()) {
call_user_func($this->callback);
}
return $result;
}
}
This decorator could be added to any existing stream and used like so:
.. code-block:: php
use GuzzleHttp\Stream\Stream;
$original = Stream::factory('foo');
$eofStream = new EofCallbackStream($original, function () {
echo 'EOF!';
});
$eofStream->read(2);
$eofStream->read(1);
// echoes "EOF!"
$eofStream->seek(0);
$eofStream->read(3);
// echoes "EOF!"

View file

@ -1,232 +0,0 @@
======================
Testing Guzzle Clients
======================
Guzzle provides several tools that will enable you to easily mock the HTTP
layer without needing to send requests over the internet.
* Mock subscriber
* Mock handler
* Node.js web server for integration testing
Mock Subscriber
===============
When testing HTTP clients, you often need to simulate specific scenarios like
returning a successful response, returning an error, or returning specific
responses in a certain order. Because unit tests need to be predictable, easy
to bootstrap, and fast, hitting an actual remote API is a test smell.
Guzzle provides a mock subscriber that can be attached to clients or requests
that allows you to queue up a list of responses to use rather than hitting a
remote API.
.. code-block:: php
use GuzzleHttp\Client;
use GuzzleHttp\Subscriber\Mock;
use GuzzleHttp\Message\Response;
$client = new Client();
// Create a mock subscriber and queue two responses.
$mock = new Mock([
new Response(200, ['X-Foo' => 'Bar']), // Use response object
"HTTP/1.1 202 OK\r\nContent-Length: 0\r\n\r\n" // Use a response string
]);
// Add the mock subscriber to the client.
$client->getEmitter()->attach($mock);
// The first request is intercepted with the first response.
echo $client->get('/')->getStatusCode();
//> 200
// The second request is intercepted with the second response.
echo $client->get('/')->getStatusCode();
//> 202
When no more responses are in the queue and a request is sent, an
``OutOfBoundsException`` is thrown.
History Subscriber
==================
When using things like the ``Mock`` subscriber, you often need to know if the
requests you expected to send were sent exactly as you intended. While the mock
subscriber responds with mocked responses, the ``GuzzleHttp\Subscriber\History``
subscriber maintains a history of the requests that were sent by a client.
.. code-block:: php
use GuzzleHttp\Client;
use GuzzleHttp\Subscriber\History;
$client = new Client();
$history = new History();
// Add the history subscriber to the client.
$client->getEmitter()->attach($history);
$client->get('http://httpbin.org/get');
$client->head('http://httpbin.org/get');
// Count the number of transactions
echo count($history);
//> 2
// Get the last request
$lastRequest = $history->getLastRequest();
// Get the last response
$lastRequest = $history->getLastResponse();
// Iterate over the transactions that were sent
foreach ($history as $transaction) {
echo $transaction['request']->getMethod();
//> GET, HEAD
echo $transaction['response']->getStatusCode();
//> 200, 200
}
The history subscriber can also be printed, revealing the requests and
responses that were sent as a string, in order.
.. code-block:: php
echo $history;
::
> GET /get HTTP/1.1
Host: httpbin.org
User-Agent: Guzzle/4.0-dev curl/7.21.4 PHP/5.5.8
< HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: application/json
Date: Tue, 25 Mar 2014 03:53:27 GMT
Server: gunicorn/0.17.4
Content-Length: 270
Connection: keep-alive
{
"headers": {
"Connection": "close",
"X-Request-Id": "3d0f7d5c-c937-4394-8248-2b8e03fcccdb",
"User-Agent": "Guzzle/4.0-dev curl/7.21.4 PHP/5.5.8",
"Host": "httpbin.org"
},
"origin": "76.104.247.1",
"args": {},
"url": "http://httpbin.org/get"
}
> HEAD /get HTTP/1.1
Host: httpbin.org
User-Agent: Guzzle/4.0-dev curl/7.21.4 PHP/5.5.8
< HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-length: 270
Content-Type: application/json
Date: Tue, 25 Mar 2014 03:53:27 GMT
Server: gunicorn/0.17.4
Connection: keep-alive
Mock Adapter
============
In addition to using the Mock subscriber, you can use the
``GuzzleHttp\Ring\Client\MockAdapter`` as the handler of a client to return the
same response over and over or return the result of a callable function.
Test Web Server
===============
Using mock responses is almost always enough when testing a web service client.
When implementing custom :doc:`HTTP handlers <handlers>`, you'll need to send
actual HTTP requests in order to sufficiently test the handler. However, a
best practice is to contact a local web server rather than a server over the
internet.
- Tests are more reliable
- Tests do not require a network connection
- Tests have no external dependencies
Using the test server
---------------------
.. warning::
The following functionality is provided to help developers of Guzzle
develop HTTP handlers. There is no promise of backwards compatibility
when it comes to the node.js test server or the ``GuzzleHttp\Tests\Server``
class. If you are using the test server or ``Server`` class outside of
guzzlehttp/guzzle, then you will need to configure autoloading and
ensure the web server is started manually.
.. hint::
You almost never need to use this test web server. You should only ever
consider using it when developing HTTP handlers. The test web server
is not necessary for mocking requests. For that, please use the
Mock subcribers and History subscriber.
Guzzle ships with a node.js test server that receives requests and returns
responses from a queue. The test server exposes a simple API that is used to
enqueue responses and inspect the requests that it has received.
Any operation on the ``Server`` object will ensure that
the server is running and wait until it is able to receive requests before
returning.
.. code-block:: php
use GuzzleHttp\Client;
use GuzzleHttp\Tests\Server;
// Start the server and queue a response
Server::enqueue("HTTP/1.1 200 OK\r\n\Content-Length: 0r\n\r\n");
$client = new Client(['base_url' => Server::$url]);
echo $client->get('/foo')->getStatusCode();
// 200
``GuzzleHttp\Tests\Server`` provides a static interface to the test server. You
can queue an HTTP response or an array of responses by calling
``Server::enqueue()``. This method accepts a string representing an HTTP
response message, a ``GuzzleHttp\Message\ResponseInterface``, or an array of
HTTP message strings / ``GuzzleHttp\Message\ResponseInterface`` objects.
.. code-block:: php
// Queue single response
Server::enqueue("HTTP/1.1 200 OK\r\n\Content-Length: 0r\n\r\n");
// Clear the queue and queue an array of responses
Server::enqueue([
"HTTP/1.1 200 OK\r\n\Content-Length: 0r\n\r\n",
"HTTP/1.1 404 Not Found\r\n\Content-Length: 0r\n\r\n"
]);
When a response is queued on the test server, the test server will remove any
previously queued responses. As the server receives requests, queued responses
are dequeued and returned to the request. When the queue is empty, the server
will return a 500 response.
You can inspect the requests that the server has retrieved by calling
``Server::received()``. This method accepts an optional ``$hydrate`` parameter
that specifies if you are retrieving an array of HTTP requests as strings or an
array of ``GuzzleHttp\Message\RequestInterface`` objects.
.. code-block:: php
foreach (Server::received() as $response) {
echo $response;
}
You can clear the list of received requests from the web server using the
``Server::flush()`` method.
.. code-block:: php
Server::flush();
echo count(Server::received());
// 0

View file

@ -1,148 +0,0 @@
<?php
namespace GuzzleHttp;
/**
* Represents the result of a batch operation. This result container is
* iterable, countable, and you can can get a result by value using the
* getResult function.
*
* Successful results are anything other than exceptions. Failure results are
* exceptions.
*
* @package GuzzleHttp
*/
class BatchResults implements \Countable, \IteratorAggregate, \ArrayAccess
{
private $hash;
/**
* @param \SplObjectStorage $hash Hash of key objects to result values.
*/
public function __construct(\SplObjectStorage $hash)
{
$this->hash = $hash;
}
/**
* Get the keys that are available on the batch result.
*
* @return array
*/
public function getKeys()
{
return iterator_to_array($this->hash);
}
/**
* Gets a result from the container for the given object. When getting
* results for a batch of requests, provide the request object.
*
* @param object $forObject Object to retrieve the result for.
*
* @return mixed|null
*/
public function getResult($forObject)
{
return isset($this->hash[$forObject]) ? $this->hash[$forObject] : null;
}
/**
* Get an array of successful results.
*
* @return array
*/
public function getSuccessful()
{
$results = [];
foreach ($this->hash as $key) {
if (!($this->hash[$key] instanceof \Exception)) {
$results[] = $this->hash[$key];
}
}
return $results;
}
/**
* Get an array of failed results.
*
* @return array
*/
public function getFailures()
{
$results = [];
foreach ($this->hash as $key) {
if ($this->hash[$key] instanceof \Exception) {
$results[] = $this->hash[$key];
}
}
return $results;
}
/**
* Allows iteration over all batch result values.
*
* @return \ArrayIterator
*/
public function getIterator()
{
$results = [];
foreach ($this->hash as $key) {
$results[] = $this->hash[$key];
}
return new \ArrayIterator($results);
}
/**
* Counts the number of elements in the batch result.
*
* @return int
*/
public function count()
{
return count($this->hash);
}
/**
* Checks if the batch contains a specific numerical array index.
*
* @param int $key Index to access
*
* @return bool
*/
public function offsetExists($key)
{
return $key < count($this->hash);
}
/**
* Allows access of the batch using a numerical array index.
*
* @param int $key Index to access.
*
* @return mixed|null
*/
public function offsetGet($key)
{
$i = -1;
foreach ($this->hash as $obj) {
if ($key === ++$i) {
return $this->hash[$obj];
}
}
return null;
}
public function offsetUnset($key)
{
throw new \RuntimeException('Not implemented');
}
public function offsetSet($key, $value)
{
throw new \RuntimeException('Not implemented');
}
}

View file

@ -1,389 +1,231 @@
<?php
namespace GuzzleHttp;
use GuzzleHttp\Event\HasEmitterTrait;
use GuzzleHttp\Message\MessageFactory;
use GuzzleHttp\Message\MessageFactoryInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\FutureResponse;
use GuzzleHttp\Ring\Client\Middleware;
use GuzzleHttp\Ring\Client\CurlMultiHandler;
use GuzzleHttp\Ring\Client\CurlHandler;
use GuzzleHttp\Ring\Client\StreamHandler;
use GuzzleHttp\Ring\Core;
use GuzzleHttp\Ring\Future\FutureInterface;
use GuzzleHttp\Exception\RequestException;
use React\Promise\FulfilledPromise;
use React\Promise\RejectedPromise;
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;
use \InvalidArgumentException as Iae;
/**
* HTTP client
* @method ResponseInterface get($uri, array $options = [])
* @method ResponseInterface head($uri, array $options = [])
* @method ResponseInterface put($uri, array $options = [])
* @method ResponseInterface post($uri, array $options = [])
* @method ResponseInterface patch($uri, array $options = [])
* @method ResponseInterface delete($uri, array $options = [])
* @method Promise\PromiseInterface getAsync($uri, array $options = [])
* @method Promise\PromiseInterface headAsync($uri, array $options = [])
* @method Promise\PromiseInterface putAsync($uri, array $options = [])
* @method Promise\PromiseInterface postAsync($uri, array $options = [])
* @method Promise\PromiseInterface patchAsync($uri, array $options = [])
* @method Promise\PromiseInterface deleteAsync($uri, array $options = [])
*/
class Client implements ClientInterface
{
use HasEmitterTrait;
/** @var MessageFactoryInterface Request factory used by the client */
private $messageFactory;
/** @var Url Base URL of the client */
private $baseUrl;
/** @var array Default request options */
private $defaults;
/** @var callable Request state machine */
private $fsm;
private $config;
/**
* Clients accept an array of constructor parameters.
*
* Here's an example of creating a client using an URI template for the
* client's base_url and an array of default request options to apply
* to each request:
* 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_url' => [
* 'http://www.foo.com/{version}/',
* ['version' => '123']
* ],
* 'defaults' => [
* 'timeout' => 10,
* 'allow_redirects' => false,
* 'proxy' => '192.168.16.1:10'
* ]
* 'base_uri' => 'http://www.foo.com/1.0/',
* 'timeout' => 0,
* 'allow_redirects' => false,
* 'proxy' => '192.168.16.1:10'
* ]);
*
* @param array $config Client configuration settings
* - base_url: Base URL of the client that is merged into relative URLs.
* Can be a string or an array that contains a URI template followed
* by an associative array of expansion variables to inject into the
* URI template.
* - handler: callable RingPHP handler used to transfer requests
* - message_factory: Factory used to create request and response object
* - defaults: Default request options to apply to each request
* - emitter: Event emitter used for request events
* - fsm: (internal use only) The request finite state machine. A
* function that accepts a transaction and optional final state. The
* function is responsible for transitioning a request through its
* lifecycle events.
* 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 = [])
{
$this->configureBaseUrl($config);
if (!isset($config['handler'])) {
$config['handler'] = HandlerStack::create();
}
// Convert the base_uri to a UriInterface
if (isset($config['base_uri'])) {
$config['base_uri'] = Psr7\uri_for($config['base_uri']);
}
$this->configureDefaults($config);
}
if (isset($config['emitter'])) {
$this->emitter = $config['emitter'];
public function __call($method, $args)
{
if (count($args) < 1) {
throw new \InvalidArgumentException('Magic request methods require a URI and optional options array');
}
$this->messageFactory = isset($config['message_factory'])
? $config['message_factory']
: new MessageFactory();
$uri = $args[0];
$opts = isset($args[1]) ? $args[1] : [];
if (isset($config['fsm'])) {
$this->fsm = $config['fsm'];
} else {
if (isset($config['handler'])) {
$handler = $config['handler'];
} elseif (isset($config['adapter'])) {
$handler = $config['adapter'];
} else {
$handler = static::getDefaultHandler();
}
$this->fsm = new RequestFsm($handler, $this->messageFactory);
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)),
$options
);
}
public function send(RequestInterface $request, array $options = [])
{
$options[RequestOptions::SYNCHRONOUS] = true;
return $this->sendAsync($request, $options)->wait();
}
public function requestAsync($method, $uri = null, 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 = null, 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)
{
if (!isset($config['base_uri'])) {
return $uri instanceof UriInterface ? $uri : new Psr7\Uri($uri);
}
return Psr7\Uri::resolve(Psr7\uri_for($config['base_uri']), $uri);
}
/**
* Create a default handler to use based on the environment
* Configures the default options for a client.
*
* @throws \RuntimeException if no viable Handler is available.
*/
public static function getDefaultHandler()
{
$default = $future = $streaming = null;
if (extension_loaded('curl')) {
$config = [
'select_timeout' => getenv('GUZZLE_CURL_SELECT_TIMEOUT') ?: 1
];
if ($maxHandles = getenv('GUZZLE_CURL_MAX_HANDLES')) {
$config['max_handles'] = $maxHandles;
}
$future = new CurlMultiHandler($config);
if (function_exists('curl_reset')) {
$default = new CurlHandler();
}
}
if (ini_get('allow_url_fopen')) {
$streaming = new StreamHandler();
}
if (!($default = ($default ?: $future) ?: $streaming)) {
throw new \RuntimeException('Guzzle requires cURL, the '
. 'allow_url_fopen ini setting, or a custom HTTP handler.');
}
$handler = $default;
if ($streaming && $streaming !== $default) {
$handler = Middleware::wrapStreaming($default, $streaming);
}
if ($future) {
$handler = Middleware::wrapFuture($handler, $future);
}
return $handler;
}
/**
* Get the default User-Agent string to use with Guzzle
*
* @return string
*/
public static function getDefaultUserAgent()
{
static $defaultAgent = '';
if (!$defaultAgent) {
$defaultAgent = 'Guzzle/' . self::VERSION;
if (extension_loaded('curl')) {
$defaultAgent .= ' curl/' . curl_version()['version'];
}
$defaultAgent .= ' PHP/' . PHP_VERSION;
}
return $defaultAgent;
}
public function getDefaultOption($keyOrPath = null)
{
return $keyOrPath === null
? $this->defaults
: Utils::getPath($this->defaults, $keyOrPath);
}
public function setDefaultOption($keyOrPath, $value)
{
Utils::setPath($this->defaults, $keyOrPath, $value);
}
public function getBaseUrl()
{
return (string) $this->baseUrl;
}
public function createRequest($method, $url = null, array $options = [])
{
$options = $this->mergeDefaults($options);
// Use a clone of the client's emitter
$options['config']['emitter'] = clone $this->getEmitter();
$url = $url || (is_string($url) && strlen($url))
? $this->buildUrl($url)
: (string) $this->baseUrl;
return $this->messageFactory->createRequest($method, $url, $options);
}
public function get($url = null, $options = [])
{
return $this->send($this->createRequest('GET', $url, $options));
}
public function head($url = null, array $options = [])
{
return $this->send($this->createRequest('HEAD', $url, $options));
}
public function delete($url = null, array $options = [])
{
return $this->send($this->createRequest('DELETE', $url, $options));
}
public function put($url = null, array $options = [])
{
return $this->send($this->createRequest('PUT', $url, $options));
}
public function patch($url = null, array $options = [])
{
return $this->send($this->createRequest('PATCH', $url, $options));
}
public function post($url = null, array $options = [])
{
return $this->send($this->createRequest('POST', $url, $options));
}
public function options($url = null, array $options = [])
{
return $this->send($this->createRequest('OPTIONS', $url, $options));
}
public function send(RequestInterface $request)
{
$isFuture = $request->getConfig()->get('future');
$trans = new Transaction($this, $request, $isFuture);
$fn = $this->fsm;
try {
$fn($trans);
if ($isFuture) {
// Turn the normal response into a future if needed.
return $trans->response instanceof FutureInterface
? $trans->response
: new FutureResponse(new FulfilledPromise($trans->response));
}
// Resolve deep futures if this is not a future
// transaction. This accounts for things like retries
// that do not have an immediate side-effect.
while ($trans->response instanceof FutureInterface) {
$trans->response = $trans->response->wait();
}
return $trans->response;
} catch (\Exception $e) {
if ($isFuture) {
// Wrap the exception in a promise
return new FutureResponse(new RejectedPromise($e));
}
throw RequestException::wrapException($trans->request, $e);
}
}
/**
* Get an array of default options to apply to the client
* @param array $config
*
* @return array
*/
protected function getDefaultOptions()
private function configureDefaults(array $config)
{
$settings = [
'allow_redirects' => true,
'exceptions' => true,
$defaults = [
'allow_redirects' => RedirectMiddleware::$defaultSettings,
'http_errors' => true,
'decode_content' => true,
'verify' => true
'verify' => true,
'cookies' => false
];
// Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set
if ($proxy = getenv('HTTP_PROXY')) {
$settings['proxy']['http'] = $proxy;
$defaults['proxy']['http'] = $proxy;
}
if ($proxy = getenv('HTTPS_PROXY')) {
$settings['proxy']['https'] = $proxy;
$defaults['proxy']['https'] = $proxy;
}
return $settings;
}
$this->config = $config + $defaults;
/**
* Expand a URI template and inherit from the base URL if it's relative
*
* @param string|array $url URL or an array of the URI template to expand
* followed by a hash of template varnames.
* @return string
* @throws \InvalidArgumentException
*/
private function buildUrl($url)
{
// URI template (absolute or relative)
if (!is_array($url)) {
return strpos($url, '://')
? (string) $url
: (string) $this->baseUrl->combine($url);
if (!empty($config['cookies']) && $config['cookies'] === true) {
$this->config['cookies'] = new CookieJar();
}
if (!isset($url[1])) {
throw new \InvalidArgumentException('You must provide a hash of '
. 'varname options in the second element of a URL array.');
}
// Absolute URL
if (strpos($url[0], '://')) {
return Utils::uriTemplate($url[0], $url[1]);
}
// Combine the relative URL with the base URL
return (string) $this->baseUrl->combine(
Utils::uriTemplate($url[0], $url[1])
);
}
private function configureBaseUrl(&$config)
{
if (!isset($config['base_url'])) {
$this->baseUrl = new Url('', '');
} elseif (!is_array($config['base_url'])) {
$this->baseUrl = Url::fromString($config['base_url']);
} elseif (count($config['base_url']) < 2) {
throw new \InvalidArgumentException('You must provide a hash of '
. 'varname options in the second element of a base_url array.');
// Add the default user-agent header.
if (!isset($this->config['headers'])) {
$this->config['headers'] = ['User-Agent' => default_user_agent()];
} else {
$this->baseUrl = Url::fromString(
Utils::uriTemplate(
$config['base_url'][0],
$config['base_url'][1]
)
);
$config['base_url'] = (string) $this->baseUrl;
}
}
private function configureDefaults($config)
{
if (!isset($config['defaults'])) {
$this->defaults = $this->getDefaultOptions();
} else {
$this->defaults = array_replace(
$this->getDefaultOptions(),
$config['defaults']
);
}
// Add the default user-agent header
if (!isset($this->defaults['headers'])) {
$this->defaults['headers'] = [
'User-Agent' => static::getDefaultUserAgent()
];
} elseif (!Core::hasHeader($this->defaults, 'User-Agent')) {
// Add the User-Agent header if one was not already set
$this->defaults['headers']['User-Agent'] = static::getDefaultUserAgent();
// 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 passed by reference.
* Merges default options into the array.
*
* @param array $options Options to modify by reference
*
* @return array
*/
private function mergeDefaults($options)
private function prepareDefaults($options)
{
$defaults = $this->defaults;
$defaults = $this->config;
// Case-insensitively merge in default headers if both defaults and
// options have headers specified.
if (!empty($defaults['headers']) && !empty($options['headers'])) {
// Create a set of lowercased keys that are present.
$lkeys = [];
foreach (array_keys($options['headers']) as $k) {
$lkeys[strtolower($k)] = true;
}
// Merge in lowercase default keys when not present in above set.
foreach ($defaults['headers'] as $key => $value) {
if (!isset($lkeys[strtolower($key)])) {
$options['headers'][$key] = $value;
}
}
// No longer need to merge in headers.
if (!empty($defaults['headers'])) {
// Default headers are only added if they are not present.
$defaults['_conditional'] = $defaults['headers'];
unset($defaults['headers']);
}
$result = array_replace_recursive($defaults, $options);
foreach ($options as $k => $v) {
// 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]);
}
@ -393,11 +235,159 @@ class Client implements ClientInterface
}
/**
* @deprecated Use {@see GuzzleHttp\Pool} instead.
* @see GuzzleHttp\Pool
* 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
*/
public function sendAll($requests, array $options = [])
private function transfer(RequestInterface $request, array $options)
{
Pool::send($this, $requests, $options);
// save_to -> sink
if (isset($options['save_to'])) {
$options['sink'] = $options['save_to'];
unset($options['save_to']);
}
// exceptions -> http_error
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 = [];
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'], null, '&');
unset($options['form_params']);
$options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded';
}
if (isset($options['multipart'])) {
$elements = $options['multipart'];
unset($options['multipart']);
$options['body'] = new Psr7\MultipartStream($elements);
// Use a multipart/form-data POST if a Content-Type is not set.
$options['_conditional']['Content-Type'] = 'multipart/form-data; boundary='
. $options['body']->getBoundary();
}
if (!empty($options['decode_content'])
&& $options['decode_content'] !== true
) {
$modify['set_headers']['Accept-Encoding'] = $options['decode_content'];
}
if (isset($options['headers'])) {
if (isset($modify['set_headers'])) {
$modify['set_headers'] = $options['headers'] + $modify['set_headers'];
} else {
$modify['set_headers'] = $options['headers'];
}
unset($options['headers']);
}
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'])) {
$value = $options['auth'];
$type = is_array($value)
? (isset($value[2]) ? strtolower($value[2]) : 'basic')
: $value;
$config['auth'] = $value;
switch (strtolower($type)) {
case 'basic':
$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;
}
}
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 Iae('query must be a string or array');
}
$modify['query'] = $value;
unset($options['query']);
}
if (isset($options['json'])) {
$modify['body'] = Psr7\stream_for(json_encode($options['json']));
$options['_conditional']['Content-Type'] = 'application/json';
unset($options['json']);
}
$request = Psr7\modify_request($request, $modify);
// 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 a the "multipart" '
. 'request option to send a multipart/form-data request.');
}
}

View file

@ -1,150 +1,84 @@
<?php
namespace GuzzleHttp;
use GuzzleHttp\Event\HasEmitterInterface;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
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
* Client interface for sending HTTP requests.
*/
interface ClientInterface extends HasEmitterInterface
interface ClientInterface
{
const VERSION = '5.2.0';
const VERSION = '6.0.2';
/**
* Create and return a new {@see RequestInterface} object.
* 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|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
* @param string $method HTTP method
* @param string|UriInterface $uri URI object or string.
* @param array $options Request options to apply.
*
* @return RequestInterface
* @return PromiseInterface
*/
public function createRequest($method, $url = null, array $options = []);
public function requestAsync($method, $uri, array $options = []);
/**
* Send a GET request
* Get a client configuration option.
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
* 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.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function get($url = null, $options = []);
/**
* Send a HEAD request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function head($url = null, array $options = []);
/**
* Send a DELETE request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function delete($url = null, array $options = []);
/**
* Send a PUT request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function put($url = null, array $options = []);
/**
* Send a PATCH request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function patch($url = null, array $options = []);
/**
* Send a POST request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function post($url = null, array $options = []);
/**
* Send an OPTIONS request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function options($url = null, array $options = []);
/**
* Sends a single request
*
* @param RequestInterface $request Request to send
*
* @return \GuzzleHttp\Message\ResponseInterface
* @throws \LogicException When the handler does not populate a response
* @throws RequestException When an error is encountered
*/
public function send(RequestInterface $request);
/**
* Get default request options of the client.
*
* @param string|null $keyOrPath The Path to a particular default request
* option to retrieve or pass null to retrieve all default request
* options. The syntax uses "/" to denote a path through nested PHP
* arrays. For example, "headers/content-type".
* @param string|null $option The config option to retrieve.
*
* @return mixed
*/
public function getDefaultOption($keyOrPath = null);
/**
* Set a default request option on the client so that any request created
* by the client will use the provided default value unless overridden
* explicitly when creating a request.
*
* @param string|null $keyOrPath The Path to a particular configuration
* value to set. The syntax uses a path notation that allows you to
* specify nested configuration values (e.g., 'headers/content-type').
* @param mixed $value Default request option value to set
*/
public function setDefaultOption($keyOrPath, $value);
/**
* Get the base URL of the client.
*
* @return string Returns the base URL if present
*/
public function getBaseUrl();
public function getConfig($option = null);
}

View file

@ -1,236 +0,0 @@
<?php
namespace GuzzleHttp;
/**
* Key value pair collection object
*/
class Collection implements
\ArrayAccess,
\IteratorAggregate,
\Countable,
ToArrayInterface
{
use HasDataTrait;
/**
* @param array $data Associative array of data to set
*/
public function __construct(array $data = [])
{
$this->data = $data;
}
/**
* Create a new collection from an array, validate the keys, and add default
* values where missing
*
* @param array $config Configuration values to apply.
* @param array $defaults Default parameters
* @param array $required Required parameter names
*
* @return self
* @throws \InvalidArgumentException if a parameter is missing
*/
public static function fromConfig(
array $config = [],
array $defaults = [],
array $required = []
) {
$data = $config + $defaults;
if ($missing = array_diff($required, array_keys($data))) {
throw new \InvalidArgumentException(
'Config is missing the following keys: ' .
implode(', ', $missing));
}
return new self($data);
}
/**
* Removes all key value pairs
*/
public function clear()
{
$this->data = [];
}
/**
* Get a specific key value.
*
* @param string $key Key to retrieve.
*
* @return mixed|null Value of the key or NULL
*/
public function get($key)
{
return isset($this->data[$key]) ? $this->data[$key] : null;
}
/**
* Set a key value pair
*
* @param string $key Key to set
* @param mixed $value Value to set
*/
public function set($key, $value)
{
$this->data[$key] = $value;
}
/**
* Add a value to a key. If a key of the same name has already been added,
* the key value will be converted into an array and the new value will be
* pushed to the end of the array.
*
* @param string $key Key to add
* @param mixed $value Value to add to the key
*/
public function add($key, $value)
{
if (!array_key_exists($key, $this->data)) {
$this->data[$key] = $value;
} elseif (is_array($this->data[$key])) {
$this->data[$key][] = $value;
} else {
$this->data[$key] = array($this->data[$key], $value);
}
}
/**
* Remove a specific key value pair
*
* @param string $key A key to remove
*/
public function remove($key)
{
unset($this->data[$key]);
}
/**
* Get all keys in the collection
*
* @return array
*/
public function getKeys()
{
return array_keys($this->data);
}
/**
* Returns whether or not the specified key is present.
*
* @param string $key The key for which to check the existence.
*
* @return bool
*/
public function hasKey($key)
{
return array_key_exists($key, $this->data);
}
/**
* Checks if any keys contains a certain value
*
* @param string $value Value to search for
*
* @return mixed Returns the key if the value was found FALSE if the value
* was not found.
*/
public function hasValue($value)
{
return array_search($value, $this->data, true);
}
/**
* Replace the data of the object with the value of an array
*
* @param array $data Associative array of data
*/
public function replace(array $data)
{
$this->data = $data;
}
/**
* Add and merge in a Collection or array of key value pair data.
*
* @param Collection|array $data Associative array of key value pair data
*/
public function merge($data)
{
foreach ($data as $key => $value) {
$this->add($key, $value);
}
}
/**
* Overwrite key value pairs in this collection with all of the data from
* an array or collection.
*
* @param array|\Traversable $data Values to override over this config
*/
public function overwriteWith($data)
{
if (is_array($data)) {
$this->data = $data + $this->data;
} elseif ($data instanceof Collection) {
$this->data = $data->toArray() + $this->data;
} else {
foreach ($data as $key => $value) {
$this->data[$key] = $value;
}
}
}
/**
* Returns a Collection containing all the elements of the collection after
* applying the callback function to each one.
*
* The callable should accept three arguments:
* - (string) $key
* - (string) $value
* - (array) $context
*
* The callable must return a the altered or unaltered value.
*
* @param callable $closure Map function to apply
* @param array $context Context to pass to the callable
*
* @return Collection
*/
public function map(callable $closure, array $context = [])
{
$collection = new static();
foreach ($this as $key => $value) {
$collection[$key] = $closure($key, $value, $context);
}
return $collection;
}
/**
* Iterates over each key value pair in the collection passing them to the
* callable. If the callable returns true, the current value from input is
* returned into the result Collection.
*
* The callable must accept two arguments:
* - (string) $key
* - (string) $value
*
* @param callable $closure Evaluation function
*
* @return Collection
*/
public function filter(callable $closure)
{
$collection = new static();
foreach ($this->data as $key => $value) {
if ($closure($key, $value)) {
$collection[$key] = $value;
}
}
return $collection;
}
}

View file

@ -1,14 +1,13 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\ToArrayInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Cookie jar that stores cookies an an array
*/
class CookieJar implements CookieJarInterface, ToArrayInterface
class CookieJar implements CookieJarInterface
{
/** @var SetCookie[] Loaded cookie data */
private $cookies = [];
@ -194,23 +193,24 @@ class CookieJar implements CookieJarInterface, ToArrayInterface
RequestInterface $request,
ResponseInterface $response
) {
if ($cookieHeader = $response->getHeaderAsArray('Set-Cookie')) {
if ($cookieHeader = $response->getHeader('Set-Cookie')) {
foreach ($cookieHeader as $cookie) {
$sc = SetCookie::fromString($cookie);
if (!$sc->getDomain()) {
$sc->setDomain($request->getHost());
$sc->setDomain($request->getUri()->getHost());
}
$this->setCookie($sc);
}
}
}
public function addCookieHeader(RequestInterface $request)
public function withCookieHeader(RequestInterface $request)
{
$values = [];
$scheme = $request->getScheme();
$host = $request->getHost();
$path = $request->getPath();
$uri = $request->getUri();
$scheme = $uri->getScheme();
$host = $uri->getHost();
$path = $uri->getPath() ?: '/';
foreach ($this->cookies as $cookie) {
if ($cookie->matchesPath($path) &&
@ -223,9 +223,9 @@ class CookieJar implements CookieJarInterface, ToArrayInterface
}
}
if ($values) {
$request->setHeader('Cookie', implode('; ', $values));
}
return $values
? $request->withHeader('Cookie', implode('; ', $values))
: $request;
}
/**

View file

@ -1,8 +1,8 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Stores HTTP cookies.
@ -17,14 +17,16 @@ use GuzzleHttp\Message\ResponseInterface;
interface CookieJarInterface extends \Countable, \IteratorAggregate
{
/**
* Add a Cookie header to a request.
* 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.
* header is added to the request and the same request is returned.
*
* @param RequestInterface $request Request object to update
* @param RequestInterface $request Request object to modify.
*
* @return RequestInterface returns the modified request.
*/
public function addCookieHeader(RequestInterface $request);
public function withCookieHeader(RequestInterface $request);
/**
* Extract cookies from an HTTP response and store them in the CookieJar.
@ -72,4 +74,11 @@ interface CookieJarInterface extends \Countable, \IteratorAggregate
* to RFC 2965.
*/
public function clearSessionCookies();
/**
* Converts the cookie jar to an array.
*
* @return array
*/
public function toArray();
}

View file

@ -1,8 +1,6 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\Utils;
/**
* Persists non-session cookies using a JSON formatted file
*/
@ -45,15 +43,14 @@ class FileCookieJar extends CookieJar
{
$json = [];
foreach ($this as $cookie) {
/** @var SetCookie $cookie */
if ($cookie->getExpires() && !$cookie->getDiscard()) {
$json[] = $cookie->toArray();
}
}
if (false === file_put_contents($filename, json_encode($json))) {
// @codeCoverageIgnoreStart
throw new \RuntimeException("Unable to save file {$filename}");
// @codeCoverageIgnoreEnd
}
}
@ -69,14 +66,12 @@ class FileCookieJar extends CookieJar
{
$json = file_get_contents($filename);
if (false === $json) {
// @codeCoverageIgnoreStart
throw new \RuntimeException("Unable to load file {$filename}");
// @codeCoverageIgnoreEnd
}
$data = Utils::jsonDecode($json, true);
$data = json_decode($json, true);
if (is_array($data)) {
foreach (Utils::jsonDecode($json, true) as $cookie) {
foreach (json_decode($json, true) as $cookie) {
$this->setCookie(new SetCookie($cookie));
}
} elseif (strlen($data)) {

View file

@ -1,8 +1,6 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\Utils;
/**
* Persists cookies in the client session
*/
@ -37,6 +35,7 @@ class SessionCookieJar extends CookieJar
{
$json = [];
foreach ($this as $cookie) {
/** @var SetCookie $cookie */
if ($cookie->getExpires() && !$cookie->getDiscard()) {
$json[] = $cookie->toArray();
}
@ -54,7 +53,7 @@ class SessionCookieJar extends CookieJar
? $_SESSION[$this->sessionKey]
: null;
$data = Utils::jsonDecode($cookieJar, true);
$data = json_decode($cookieJar, true);
if (is_array($data)) {
foreach ($data as $cookie) {
$this->setCookie(new SetCookie($cookie));

View file

@ -1,12 +1,10 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\ToArrayInterface;
/**
* Set-Cookie object
*/
class SetCookie implements ToArrayInterface
class SetCookie
{
/** @var array */
private static $defaults = [
@ -350,8 +348,8 @@ class SetCookie implements ToArrayInterface
}
// Check if any of the invalid characters are present in the cookie name
if (preg_match("/[=,; \t\r\n\013\014]/", $name)) {
return "Cookie name must not cannot invalid characters: =,; \\t\\r\\n\\013\\014";
if (preg_match('/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5b-\x5d\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

View file

@ -1,20 +0,0 @@
<?php
namespace GuzzleHttp\Event;
/**
* Basic event class that can be extended.
*/
abstract class AbstractEvent implements EventInterface
{
private $propagationStopped = false;
public function isPropagationStopped()
{
return $this->propagationStopped;
}
public function stopPropagation()
{
$this->propagationStopped = true;
}
}

View file

@ -1,61 +0,0 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Transaction;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Message\RequestInterface;
/**
* Base class for request events, providing a request and client getter.
*/
abstract class AbstractRequestEvent extends AbstractEvent
{
/** @var Transaction */
protected $transaction;
/**
* @param Transaction $transaction
*/
public function __construct(Transaction $transaction)
{
$this->transaction = $transaction;
}
/**
* Get the HTTP client associated with the event.
*
* @return ClientInterface
*/
public function getClient()
{
return $this->transaction->client;
}
/**
* Get the request object
*
* @return RequestInterface
*/
public function getRequest()
{
return $this->transaction->request;
}
/**
* Get the number of transaction retries.
*
* @return int
*/
public function getRetryCount()
{
return $this->transaction->retries;
}
/**
* @return Transaction
*/
protected function getTransaction()
{
return $this->transaction;
}
}

View file

@ -1,40 +0,0 @@
<?php
namespace GuzzleHttp\Event;
/**
* Abstract request event that can be retried.
*/
class AbstractRetryableEvent extends AbstractTransferEvent
{
/**
* Mark the request as needing a retry and stop event propagation.
*
* This action allows you to retry a request without emitting the "end"
* event multiple times for a given request. When retried, the request
* emits a before event and is then sent again using the client that sent
* the original request.
*
* When retrying, it is important to limit the number of retries you allow
* to prevent infinite loops.
*
* This action can only be taken during the "complete" and "error" events.
*
* @param int $afterDelay If specified, the amount of time in milliseconds
* to delay before retrying. Note that this must
* be supported by the underlying RingPHP handler
* to work properly. Set to 0 or provide no value
* to retry immediately.
*/
public function retry($afterDelay = 0)
{
// Setting the transition state to 'retry' will cause the next state
// transition of the transaction to retry the request.
$this->transaction->state = 'retry';
if ($afterDelay) {
$this->transaction->request->getConfig()->set('delay', $afterDelay);
}
$this->stopPropagation();
}
}

View file

@ -1,63 +0,0 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\Ring\Future\FutureInterface;
/**
* Event that contains transfer statistics, and can be intercepted.
*/
abstract class AbstractTransferEvent extends AbstractRequestEvent
{
/**
* Get all transfer information as an associative array if no $name
* argument is supplied, or gets a specific transfer statistic if
* a $name attribute is supplied (e.g., 'total_time').
*
* @param string $name Name of the transfer stat to retrieve
*
* @return mixed|null|array
*/
public function getTransferInfo($name = null)
{
if (!$name) {
return $this->transaction->transferInfo;
}
return isset($this->transaction->transferInfo[$name])
? $this->transaction->transferInfo[$name]
: null;
}
/**
* Returns true/false if a response is available.
*
* @return bool
*/
public function hasResponse()
{
return !($this->transaction->response instanceof FutureInterface);
}
/**
* Get the response.
*
* @return ResponseInterface|null
*/
public function getResponse()
{
return $this->hasResponse() ? $this->transaction->response : null;
}
/**
* Intercept the request and associate a response
*
* @param ResponseInterface $response Response to set
*/
public function intercept(ResponseInterface $response)
{
$this->transaction->response = $response;
$this->transaction->exception = null;
$this->stopPropagation();
}
}

View file

@ -1,26 +0,0 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Message\ResponseInterface;
/**
* Event object emitted before a request is sent.
*
* This event MAY be emitted multiple times (i.e., if a request is retried).
* You MAY change the Response associated with the request using the
* intercept() method of the event.
*/
class BeforeEvent extends AbstractRequestEvent
{
/**
* Intercept the request and associate a response
*
* @param ResponseInterface $response Response to set
*/
public function intercept(ResponseInterface $response)
{
$this->transaction->response = $response;
$this->transaction->exception = null;
$this->stopPropagation();
}
}

View file

@ -1,14 +0,0 @@
<?php
namespace GuzzleHttp\Event;
/**
* Event object emitted after a request has been completed.
*
* This event MAY be emitted multiple times for a single request. You MAY
* change the Response associated with the request using the intercept()
* method of the event.
*
* This event allows the request to be retried if necessary using the retry()
* method of the event.
*/
class CompleteEvent extends AbstractRetryableEvent {}

View file

@ -1,146 +0,0 @@
<?php
namespace GuzzleHttp\Event;
/**
* Guzzle event emitter.
*
* Some of this class is based on the Symfony EventDispatcher component, which
* ships with the following license:
*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://github.com/symfony/symfony/tree/master/src/Symfony/Component/EventDispatcher
*/
class Emitter implements EmitterInterface
{
/** @var array */
private $listeners = [];
/** @var array */
private $sorted = [];
public function on($eventName, callable $listener, $priority = 0)
{
if ($priority === 'first') {
$priority = isset($this->listeners[$eventName])
? max(array_keys($this->listeners[$eventName])) + 1
: 1;
} elseif ($priority === 'last') {
$priority = isset($this->listeners[$eventName])
? min(array_keys($this->listeners[$eventName])) - 1
: -1;
}
$this->listeners[$eventName][$priority][] = $listener;
unset($this->sorted[$eventName]);
}
public function once($eventName, callable $listener, $priority = 0)
{
$onceListener = function (
EventInterface $event,
$eventName
) use (&$onceListener, $eventName, $listener, $priority) {
$this->removeListener($eventName, $onceListener);
$listener($event, $eventName, $this);
};
$this->on($eventName, $onceListener, $priority);
}
public function removeListener($eventName, callable $listener)
{
if (empty($this->listeners[$eventName])) {
return;
}
foreach ($this->listeners[$eventName] as $priority => $listeners) {
if (false !== ($key = array_search($listener, $listeners, true))) {
unset(
$this->listeners[$eventName][$priority][$key],
$this->sorted[$eventName]
);
}
}
}
public function listeners($eventName = null)
{
// Return all events in a sorted priority order
if ($eventName === null) {
foreach (array_keys($this->listeners) as $eventName) {
if (empty($this->sorted[$eventName])) {
$this->listeners($eventName);
}
}
return $this->sorted;
}
// Return the listeners for a specific event, sorted in priority order
if (empty($this->sorted[$eventName])) {
$this->sorted[$eventName] = [];
if (isset($this->listeners[$eventName])) {
krsort($this->listeners[$eventName], SORT_NUMERIC);
foreach ($this->listeners[$eventName] as $listeners) {
foreach ($listeners as $listener) {
$this->sorted[$eventName][] = $listener;
}
}
}
}
return $this->sorted[$eventName];
}
public function hasListeners($eventName)
{
return !empty($this->listeners[$eventName]);
}
public function emit($eventName, EventInterface $event)
{
if (isset($this->listeners[$eventName])) {
foreach ($this->listeners($eventName) as $listener) {
$listener($event, $eventName);
if ($event->isPropagationStopped()) {
break;
}
}
}
return $event;
}
public function attach(SubscriberInterface $subscriber)
{
foreach ($subscriber->getEvents() as $eventName => $listeners) {
if (is_array($listeners[0])) {
foreach ($listeners as $listener) {
$this->on(
$eventName,
[$subscriber, $listener[0]],
isset($listener[1]) ? $listener[1] : 0
);
}
} else {
$this->on(
$eventName,
[$subscriber, $listeners[0]],
isset($listeners[1]) ? $listeners[1] : 0
);
}
}
}
public function detach(SubscriberInterface $subscriber)
{
foreach ($subscriber->getEvents() as $eventName => $listener) {
$this->removeListener($eventName, [$subscriber, $listener[0]]);
}
}
}

View file

@ -1,96 +0,0 @@
<?php
namespace GuzzleHttp\Event;
/**
* Guzzle event emitter.
*/
interface EmitterInterface
{
/**
* Binds a listener to a specific event.
*
* @param string $eventName Name of the event to bind to.
* @param callable $listener Listener to invoke when triggered.
* @param int|string $priority The higher this value, the earlier an event
* listener will be triggered in the chain (defaults to 0). You can
* pass "first" or "last" to dynamically specify the event priority
* based on the current event priorities associated with the given
* event name in the emitter. Use "first" to set the priority to the
* current highest priority plus one. Use "last" to set the priority to
* the current lowest event priority minus one.
*/
public function on($eventName, callable $listener, $priority = 0);
/**
* Binds a listener to a specific event. After the listener is triggered
* once, it is removed as a listener.
*
* @param string $eventName Name of the event to bind to.
* @param callable $listener Listener to invoke when triggered.
* @param int $priority The higher this value, the earlier an event
* listener will be triggered in the chain (defaults to 0)
*/
public function once($eventName, callable $listener, $priority = 0);
/**
* Removes an event listener from the specified event.
*
* @param string $eventName The event to remove a listener from
* @param callable $listener The listener to remove
*/
public function removeListener($eventName, callable $listener);
/**
* Gets the listeners of a specific event or all listeners if no event is
* specified.
*
* @param string $eventName The name of the event. Pass null (the default)
* to retrieve all listeners.
*
* @return array The event listeners for the specified event, or all event
* listeners by event name. The format of the array when retrieving a
* specific event list is an array of callables. The format of the array
* when retrieving all listeners is an associative array of arrays of
* callables.
*/
public function listeners($eventName = null);
/**
* Checks if the emitter has listeners by the given name.
*
* @param string $eventName The name of the event to check.
*
* @return bool
*/
public function hasListeners($eventName);
/**
* Emits an event to all registered listeners.
*
* Each event that is bound to the emitted eventName receives a
* EventInterface, the name of the event, and the event emitter.
*
* @param string $eventName The name of the event to dispatch.
* @param EventInterface $event The event to pass to the event handlers/listeners.
*
* @return EventInterface Returns the provided event object
*/
public function emit($eventName, EventInterface $event);
/**
* Attaches an event subscriber.
*
* The subscriber is asked for all the events it is interested in and added
* as an event listener for each event.
*
* @param SubscriberInterface $subscriber Subscriber to attach.
*/
public function attach(SubscriberInterface $subscriber);
/**
* Detaches an event subscriber.
*
* @param SubscriberInterface $subscriber Subscriber to detach.
*/
public function detach(SubscriberInterface $subscriber);
}

View file

@ -1,28 +0,0 @@
<?php
namespace GuzzleHttp\Event;
/**
* A terminal event that is emitted when a request transaction has ended.
*
* This event is emitted for both successful responses and responses that
* encountered an exception. You need to check if an exception is present
* in your listener to know the difference.
*
* You MAY intercept the response associated with the event if needed, but keep
* in mind that the "complete" event will not be triggered as a result.
*/
class EndEvent extends AbstractTransferEvent
{
/**
* Get the exception that was encountered (if any).
*
* This method should be used to check if the request was sent successfully
* or if it encountered errors.
*
* @return \Exception|null
*/
public function getException()
{
return $this->transaction->exception;
}
}

View file

@ -1,27 +0,0 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Exception\RequestException;
/**
* Event emitted when an error occurs while sending a request.
*
* This event MAY be emitted multiple times. You MAY intercept the exception
* and inject a response into the event to rescue the request using the
* intercept() method of the event.
*
* This event allows the request to be retried using the "retry" method of the
* event.
*/
class ErrorEvent extends AbstractRetryableEvent
{
/**
* Get the exception that was encountered
*
* @return RequestException
*/
public function getException()
{
return $this->transaction->exception;
}
}

View file

@ -1,23 +0,0 @@
<?php
namespace GuzzleHttp\Event;
/**
* Base event interface used when dispatching events to listeners using an
* event emitter.
*/
interface EventInterface
{
/**
* Returns whether or not stopPropagation was called on the event.
*
* @return bool
* @see Event::stopPropagation
*/
public function isPropagationStopped();
/**
* Stops the propagation of the event, preventing subsequent listeners
* registered to the same event from being invoked.
*/
public function stopPropagation();
}

View file

@ -1,15 +0,0 @@
<?php
namespace GuzzleHttp\Event;
/**
* Holds an event emitter
*/
interface HasEmitterInterface
{
/**
* Get the event emitter of the object
*
* @return EmitterInterface
*/
public function getEmitter();
}

View file

@ -1,20 +0,0 @@
<?php
namespace GuzzleHttp\Event;
/**
* Trait that implements the methods of HasEmitterInterface
*/
trait HasEmitterTrait
{
/** @var EmitterInterface */
private $emitter;
public function getEmitter()
{
if (!$this->emitter) {
$this->emitter = new Emitter();
}
return $this->emitter;
}
}

View file

@ -1,88 +0,0 @@
<?php
namespace GuzzleHttp\Event;
/**
* Trait that provides methods for extract event listeners specified in an array
* and attaching them to an emitter owned by the object or one of its direct
* dependencies.
*/
trait ListenerAttacherTrait
{
/**
* Attaches event listeners and properly sets their priorities and whether
* or not they are are only executed once.
*
* @param HasEmitterInterface $object Object that has the event emitter.
* @param array $listeners Array of hashes representing event
* event listeners. Each item contains
* "name", "fn", "priority", & "once".
*/
private function attachListeners(HasEmitterInterface $object, array $listeners)
{
$emitter = $object->getEmitter();
foreach ($listeners as $el) {
if ($el['once']) {
$emitter->once($el['name'], $el['fn'], $el['priority']);
} else {
$emitter->on($el['name'], $el['fn'], $el['priority']);
}
}
}
/**
* Extracts the allowed events from the provided array, and ignores anything
* else in the array. The event listener must be specified as a callable or
* as an array of event listener data ("name", "fn", "priority", "once").
*
* @param array $source Array containing callables or hashes of data to be
* prepared as event listeners.
* @param array $events Names of events to look for in the provided $source
* array. Other keys are ignored.
* @return array
*/
private function prepareListeners(array $source, array $events)
{
$listeners = [];
foreach ($events as $name) {
if (isset($source[$name])) {
$this->buildListener($name, $source[$name], $listeners);
}
}
return $listeners;
}
/**
* Creates a complete event listener definition from the provided array of
* listener data. Also works recursively if more than one listeners are
* contained in the provided array.
*
* @param string $name Name of the event the listener is for.
* @param array|callable $data Event listener data to prepare.
* @param array $listeners Array of listeners, passed by reference.
*
* @throws \InvalidArgumentException if the event data is malformed.
*/
private function buildListener($name, $data, &$listeners)
{
static $defaults = ['priority' => 0, 'once' => false];
// If a callable is provided, normalize it to the array format.
if (is_callable($data)) {
$data = ['fn' => $data];
}
// Prepare the listener and add it to the array, recursively.
if (isset($data['fn'])) {
$data['name'] = $name;
$listeners[] = $data + $defaults;
} elseif (is_array($data)) {
foreach ($data as $listenerData) {
$this->buildListener($name, $listenerData, $listeners);
}
} else {
throw new \InvalidArgumentException('Each event listener must be a '
. 'callable or an associative array containing a "fn" key.');
}
}
}

View file

@ -1,51 +0,0 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Transaction;
/**
* Event object emitted when upload or download progress is made.
*
* You can access the progress values using their corresponding public
* properties:
*
* - $downloadSize: The number of bytes that will be downloaded (if known)
* - $downloaded: The number of bytes that have been downloaded
* - $uploadSize: The number of bytes that will be uploaded (if known)
* - $uploaded: The number of bytes that have been uploaded
*/
class ProgressEvent extends AbstractRequestEvent
{
/** @var int Amount of data to be downloaded */
public $downloadSize;
/** @var int Amount of data that has been downloaded */
public $downloaded;
/** @var int Amount of data to upload */
public $uploadSize;
/** @var int Amount of data that has been uploaded */
public $uploaded;
/**
* @param Transaction $transaction Transaction being sent.
* @param int $downloadSize Amount of data to download (if known)
* @param int $downloaded Amount of data that has been downloaded
* @param int $uploadSize Amount of data to upload (if known)
* @param int $uploaded Amount of data that had been uploaded
*/
public function __construct(
Transaction $transaction,
$downloadSize,
$downloaded,
$uploadSize,
$uploaded
) {
parent::__construct($transaction);
$this->downloadSize = $downloadSize;
$this->downloaded = $downloaded;
$this->uploadSize = $uploadSize;
$this->uploaded = $uploaded;
}
}

View file

@ -1,56 +0,0 @@
<?php
namespace GuzzleHttp\Event;
/**
* Contains methods used to manage the request event lifecycle.
*/
final class RequestEvents
{
// Generic event priorities
const EARLY = 10000;
const LATE = -10000;
// "before" priorities
const PREPARE_REQUEST = -100;
const SIGN_REQUEST = -10000;
// "complete" and "error" response priorities
const VERIFY_RESPONSE = 100;
const REDIRECT_RESPONSE = 200;
/**
* Converts an array of event options into a formatted array of valid event
* configuration.
*
* @param array $options Event array to convert
* @param array $events Event names to convert in the options array.
* @param mixed $handler Event handler to utilize
*
* @return array
* @throws \InvalidArgumentException if the event config is invalid
* @internal
*/
public static function convertEventArray(
array $options,
array $events,
$handler
) {
foreach ($events as $name) {
if (!isset($options[$name])) {
$options[$name] = [$handler];
} elseif (is_callable($options[$name])) {
$options[$name] = [$options[$name], $handler];
} elseif (is_array($options[$name])) {
if (isset($options[$name]['fn'])) {
$options[$name] = [$options[$name], $handler];
} else {
$options[$name][] = $handler;
}
} else {
throw new \InvalidArgumentException('Invalid event format');
}
}
return $options;
}
}

View file

@ -1,34 +0,0 @@
<?php
namespace GuzzleHttp\Event;
/**
* SubscriberInterface provides an array of events to an
* EventEmitterInterface when it is registered. The emitter then binds the
* listeners specified by the EventSubscriber.
*
* This interface is based on the SubscriberInterface of the Symfony.
* @link https://github.com/symfony/symfony/tree/master/src/Symfony/Component/EventDispatcher
*/
interface SubscriberInterface
{
/**
* Returns an array of event names this subscriber wants to listen to.
*
* The returned array keys MUST map to an event name. Each array value
* MUST be an array in which the first element is the name of a function
* on the EventSubscriber OR an array of arrays in the aforementioned
* format. The second element in the array is optional, and if specified,
* designates the event priority.
*
* For example, the following are all valid:
*
* - ['eventName' => ['methodName']]
* - ['eventName' => ['methodName', $priority]]
* - ['eventName' => [['methodName'], ['otherMethod']]
* - ['eventName' => [['methodName'], ['otherMethod', $priority]]
* - ['eventName' => [['methodName', $priority], ['otherMethod', $priority]]
*
* @return array
*/
public function getEvents();
}

View file

@ -1,4 +1,37 @@
<?php
namespace GuzzleHttp\Exception;
class ConnectException extends RequestException {}
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;
}
}

View file

@ -1,4 +0,0 @@
<?php
namespace GuzzleHttp\Exception;
class CouldNotRewindStreamException extends RequestException {}

View file

@ -0,0 +1,4 @@
<?php
namespace GuzzleHttp\Exception;
interface GuzzleException {}

View file

@ -1,31 +0,0 @@
<?php
namespace GuzzleHttp\Exception;
use GuzzleHttp\Message\ResponseInterface;
/**
* Exception when a client is unable to parse the response body as XML or JSON
*/
class ParseException extends TransferException
{
/** @var ResponseInterface */
private $response;
public function __construct(
$message = '',
ResponseInterface $response = null,
\Exception $previous = null
) {
parent::__construct($message, 0, $previous);
$this->response = $response;
}
/**
* Get the associated response
*
* @return ResponseInterface|null
*/
public function getResponse()
{
return $this->response;
}
}

View file

@ -1,11 +1,9 @@
<?php
namespace GuzzleHttp\Exception;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\Ring\Exception\ConnectException;
use GuzzleHttp\Exception\ConnectException as HttpConnectException;
use GuzzleHttp\Ring\Future\FutureInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Promise\PromiseInterface;
/**
* HTTP Request exception
@ -18,23 +16,28 @@ class RequestException extends TransferException
/** @var ResponseInterface */
private $response;
/** @var array */
private $handlerContext;
public function __construct(
$message,
RequestInterface $request,
ResponseInterface $response = null,
\Exception $previous = null
\Exception $previous = null,
array $handlerContext = []
) {
// Set the code of the exception if the response is set and not future.
$code = $response && !($response instanceof FutureInterface)
$code = $response && !($response instanceof PromiseInterface)
? $response->getStatusCode()
: 0;
parent::__construct($message, $code, $previous);
$this->request = $request;
$this->response = $response;
$this->handlerContext = $handlerContext;
}
/**
* Wrap non-RequesExceptions with a RequestException
* Wrap non-RequestExceptions with a RequestException
*
* @param RequestInterface $request
* @param \Exception $e
@ -43,13 +46,9 @@ class RequestException extends TransferException
*/
public static function wrapException(RequestInterface $request, \Exception $e)
{
if ($e instanceof RequestException) {
return $e;
} elseif ($e instanceof ConnectException) {
return new HttpConnectException($e->getMessage(), $request, null, $e);
} else {
return new RequestException($e->getMessage(), $request, null, $e);
}
return $e instanceof RequestException
? $e
: new RequestException($e->getMessage(), $request, null, $e);
}
/**
@ -58,16 +57,24 @@ class RequestException extends TransferException
* @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
\Exception $previous = null,
array $ctx = []
) {
if (!$response) {
return new self('Error completing request', $request, null, $previous);
return new self(
'Error completing request',
$request,
null,
$previous,
$ctx
);
}
$level = floor($response->getStatusCode() / 100);
@ -82,11 +89,12 @@ class RequestException extends TransferException
$className = __CLASS__;
}
$message = $label . ' [url] ' . $request->getUrl()
$message = $label . ' [url] ' . $request->getUri()
. ' [http method] ' . $request->getMethod()
. ' [status code] ' . $response->getStatusCode()
. ' [reason phrase] ' . $response->getReasonPhrase();
return new $className($message, $request, $response, $previous);
return new $className($message, $request, $response, $previous, $ctx);
}
/**
@ -118,4 +126,19 @@ class RequestException extends TransferException
{
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;
}
}

View file

@ -1,12 +1,12 @@
<?php
namespace GuzzleHttp\Stream\Exception;
namespace GuzzleHttp\Exception;
use GuzzleHttp\Stream\StreamInterface;
use Psr\Http\Message\StreamInterface;
/**
* Exception thrown when a seek fails on a stream.
*/
class SeekException extends \RuntimeException
class SeekException extends \RuntimeException implements GuzzleException
{
private $stream;

View file

@ -1,4 +0,0 @@
<?php
namespace GuzzleHttp\Exception;
class StateException extends TransferException {};

View file

@ -1,4 +1,4 @@
<?php
namespace GuzzleHttp\Exception;
class TransferException extends \RuntimeException {}
class TransferException extends \RuntimeException implements GuzzleException {}

View file

@ -1,34 +0,0 @@
<?php
namespace GuzzleHttp\Exception;
use GuzzleHttp\Message\ResponseInterface;
/**
* Exception when a client is unable to parse the response body as XML
*/
class XmlParseException extends ParseException
{
/** @var \LibXMLError */
protected $error;
public function __construct(
$message = '',
ResponseInterface $response = null,
\Exception $previous = null,
\LibXMLError $error = null
) {
parent::__construct($message, $response, $previous);
$this->error = $error;
}
/**
* Get the associated error
*
* @return \LibXMLError|null
*/
public function getError()
{
return $this->error;
}
}

View file

@ -0,0 +1,507 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\LazyOpenStream;
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)
{
$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']);
if (isset($options['curl']['body_as_string'])) {
$options['_body_as_string'] = $options['curl']['body_as_string'];
unset($options['curl']['body_as_string']);
}
// Add handler options from the request configuration options
if (isset($options['curl'])) {
$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 (!$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 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 new RejectedPromise(
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 new RejectedPromise($error);
}
private function getDefaultConf(EasyHandle $easy)
{
$conf = [
'_headers' => $easy->request->getHeaders(),
CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(),
CURLOPT_URL => (string) $easy->request->getUri(),
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();
$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) {
$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'])) {
$conf[CURLOPT_CAINFO] = $options['verify'];
if (!file_exists($options['verify'])) {
throw new \InvalidArgumentException(
"SSL CA bundle not found: {$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]);
}
if (isset($options['timeout'])) {
$conf[CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
}
if (isset($options['connect_timeout'])) {
$conf[CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
}
if (isset($options['proxy'])) {
if (!is_array($options['proxy'])) {
$conf[CURLOPT_PROXY] = $options['proxy'];
} elseif ($scheme = $easy->request->getUri()->getScheme()) {
if (isset($options['proxy'][$scheme])) {
$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 = null;
} elseif (!is_callable($easy->options['on_headers'])) {
throw new \InvalidArgumentException('on_headers must be callable');
} else {
$onHeaders = $easy->options['on_headers'];
}
return function ($ch, $h) use (
$onHeaders,
$easy,
&$startingResponse
) {
$value = trim($h);
if ($value === '') {
$startingResponse = true;
$easy->createResponse();
if ($onHeaders) {
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);
};
}
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -0,0 +1,197 @@
<?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, $currentTime - $nextTime);
}
}

View file

@ -0,0 +1,87 @@
<?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'])
) {
unset($headers[$normalizedKeys['content-encoding']]);
if (isset($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]) ? (int) $startLine[2] : null
);
}
public function __get($name)
{
$msg = $name === 'handle'
? 'The EasyHandle has been released'
: 'Invalid property: ' . $name;
throw new \BadMethodCallException($msg);
}
}

View file

@ -0,0 +1,152 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\RejectedPromise;
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 MockHandler
*/
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 (is_callable($response)) {
$response = $response($request, $options);
}
$response = $response instanceof \Exception
? new RejectedPromise($response)
: \GuzzleHttp\Promise\promise_for($response);
if (!$this->onFulfilled && !$this->onRejected) {
return $response;
}
return $response->then(
function ($value) {
if ($this->onFulfilled) {
call_user_func($this->onFulfilled, $value);
}
return $value;
},
function ($reason) {
if ($this->onRejected) {
call_user_func($this->onRejected, $reason);
}
return new RejectedPromise($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 RequestInterface
*/
public function getLastOptions()
{
return $this->lastOptions;
}
/**
* Returns the number of remaining items in the queue.
*
* @return int
*/
public function count()
{
return count($this->queue);
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace GuzzleHttp\Handler;
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['sync'])
? $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);
};
}
}

View file

@ -0,0 +1,411 @@
<?php
namespace GuzzleHttp\Handler;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7;
use Psr\Http\Message\RequestInterface;
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);
}
try {
// Does not support the expect header.
$request = $request->withoutHeader('Expect');
$stream = $this->createStream($request, $options);
return $this->createResponse($request, $options, $stream);
} 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
) {
$e = new ConnectException($e->getMessage(), $request, $e);
}
return new RejectedPromise(
RequestException::wrapException($request, $e)
);
}
}
private function createResponse(
RequestInterface $request,
array $options,
$stream
) {
$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 = $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 new RejectedPromise($ex);
}
}
if ($sink !== $stream) {
$this->drain($stream, $sink);
}
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\Stream(Psr7\try_fopen($sink, 'r+'))
: 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)
);
// Remove content-encoding header
unset($headers[$normalizedKeys['content-encoding']]);
// Fix content-length header
if (isset($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
*
* @return StreamInterface
* @throws \RuntimeException when the sink option is invalid.
*/
private function drain(StreamInterface $source, StreamInterface $sink)
{
Psr7\copy_to_stream($source, $sink);
$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, $options);
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']
);
}
$context = $this->createResource(
function () use ($context, $params) {
return stream_context_create($context, $params);
}
);
return $this->createResource(
function () use ($request, &$http_response_header, $context) {
$resource = fopen($request->getUri(), 'r', null, $context);
$this->lastHeaders = $http_response_header;
return $resource;
}
);
}
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])) {
$options['http']['proxy'] = $value[$scheme];
}
}
}
private function add_timeout(RequestInterface $request, &$options, $value, &$params)
{
$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;
return;
} else {
throw new \InvalidArgumentException('Invalid verify request option');
}
$options['ssl']['verify_peer'] = 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();
$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);
}
};
}
}

View file

@ -0,0 +1,272 @@
<?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 you 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)
{
if (!$this->cached) {
$this->cached = $this->resolve();
}
$handler = $this->cached;
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 (!($prev = $this->handler)) {
throw new \LogicException('No handler has been specified');
}
foreach (array_reverse($this->stack) as $fn) {
$prev = $fn[0]($prev);
}
return $prev;
}
/**
* @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) . ')';
}
}

Some files were not shown because too many files have changed in this diff Show more