WEBSERVICE is HTTP client agnostic

HTTP client must be configured via `Settings::setHttpClient()`. This is
a small breaking change, but only for the very few people who started using
WEBSERVICE from last version.

Fixes #1562
Closes #1568
This commit is contained in:
Adrien Crivelli 2020-07-19 11:33:01 +09:00
parent 165034ad70
commit 7cb4884b96
No known key found for this signature in database
GPG Key ID: B182FD79DC6DE92E
8 changed files with 180 additions and 297 deletions

View File

@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Fixed
- nothing
- WEBSERVICE is HTTP client agnostic and must be configured via `Settings::setHttpClient()` [#1562](https://github.com/PHPOffice/PhpSpreadsheet/issues/1562)
### Changed

View File

@ -57,7 +57,8 @@
"markbaker/complex": "^1.4",
"markbaker/matrix": "^1.2",
"psr/simple-cache": "^1.0",
"guzzlehttp/guzzle": "^7.0"
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"require-dev": {
"dompdf/dompdf": "^0.8.5",

312
composer.lock generated
View File

@ -4,211 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b7ea4dea7ce2e1c2299029fe978d2173",
"content-hash": "931b86c12c78e665f1766ea922f95e0b",
"packages": [
{
"name": "guzzlehttp/guzzle",
"version": "7.0.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "2d9d3c186a6637a43193e66b097c50e4451eaab2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/2d9d3c186a6637a43193e66b097c50e4451eaab2",
"reference": "2d9d3c186a6637a43193e66b097c50e4451eaab2",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.0",
"guzzlehttp/psr7": "^1.6.1",
"php": "^7.2.5",
"psr/http-client": "^1.0"
},
"provide": {
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.0",
"ext-curl": "*",
"php-http/client-integration-tests": "dev-phpunit8",
"phpunit/phpunit": "^8.5.5",
"psr/log": "^1.1"
},
"suggest": {
"ext-curl": "Required for CURL handler support",
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "7.0-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
}
],
"description": "Guzzle is a PHP HTTP client library",
"homepage": "http://guzzlephp.org/",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"psr-18",
"psr-7",
"rest",
"web service"
],
"time": "2020-06-27T10:33:25+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "v1.3.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"shasum": ""
},
"require": {
"php": ">=5.5.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"time": "2016-12-20T10:07:11+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "1.6.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "239400de7a173fe9901b9ac7c06497751f00727a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
"reference": "239400de7a173fe9901b9ac7c06497751f00727a",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"psr/http-message": "~1.0",
"ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"ext-zlib": "*",
"phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
},
"suggest": {
"zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.6-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Schultze",
"homepage": "https://github.com/Tobion"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"time": "2019-07-01T23:21:34+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "2.1.0",
@ -482,20 +279,20 @@
},
{
"name": "psr/http-client",
"version": "1.0.0",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "496a823ef742b632934724bf769560c2a5c7c44e"
"reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/496a823ef742b632934724bf769560c2a5c7c44e",
"reference": "496a823ef742b632934724bf769560c2a5c7c44e",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
"reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
"shasum": ""
},
"require": {
"php": "^7.0",
"php": "^7.0 || ^8.0",
"psr/http-message": "^1.0"
},
"type": "library",
@ -527,7 +324,59 @@
"psr",
"psr-18"
],
"time": "2018-10-30T23:29:13+00:00"
"time": "2020-06-29T06:28:15+00:00"
},
{
"name": "psr/http-factory",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
"reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
"shasum": ""
},
"require": {
"php": ">=7.0.0",
"psr/http-message": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
"message",
"psr",
"psr-17",
"psr-7",
"request",
"response"
],
"time": "2019-04-30T12:38:16+00:00"
},
{
"name": "psr/http-message",
@ -627,46 +476,6 @@
],
"time": "2017-10-23T01:57:42+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.17.1",
@ -4382,5 +4191,6 @@
"ext-zip": "*",
"ext-zlib": "*"
},
"platform-dev": []
"platform-dev": [],
"plugin-api-version": "1.1.0"
}

View File

@ -43,3 +43,20 @@ More details of the features available once a locale has been set,
including a list of the languages and locales currently supported, can
be found in [Locale Settings for
Formulae](./recipes.md#locale-settings-for-formulae).
## HTTP client
In order to use the `WEBSERVICE` function in formulae, you must configure an
HTTP client. Assuming you chose Guzzle 7, this can be done like:
```php
use GuzzleHttp\Client;
use Http\Factory\Guzzle\RequestFactory;
use PhpOffice\PhpSpreadsheet\Settings;
$client = new Client();
$requestFactory = new RequestFactory();
Settings::setHttpClient($client, $requestFactory);
```

View File

@ -2,7 +2,6 @@
namespace PhpOffice\PhpSpreadsheet\Calculation;
use GuzzleHttp\Psr7\Request;
use PhpOffice\PhpSpreadsheet\Settings;
use Psr\Http\Client\ClientExceptionInterface;
@ -31,7 +30,8 @@ class Web
// Get results from the the webservice
$client = Settings::getHttpClient();
$request = new Request('GET', $url);
$requestFactory = Settings::getRequestFactory();
$request = $requestFactory->createRequest('GET', $url);
try {
$response = $client->sendRequest($request);
@ -43,7 +43,7 @@ class Web
return Functions::VALUE(); // cURL error
}
$output = (string) $response->getBody();
$output = $response->getBody()->getContents();
if (strlen($output) > 32767) {
return Functions::VALUE(); // Output not a string or too long
}

View File

@ -2,11 +2,11 @@
namespace PhpOffice\PhpSpreadsheet;
use GuzzleHttp\Client;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Chart\Renderer\IRenderer;
use PhpOffice\PhpSpreadsheet\Collection\Memory;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\SimpleCache\CacheInterface;
class Settings
@ -47,9 +47,14 @@ class Settings
/**
* The HTTP client implementation to be used for network request.
*
* @var ClientInterface
* @var null|ClientInterface
*/
private static $client;
private static $httpClient;
/**
* @var null|RequestFactoryInterface
*/
private static $requestFactory;
/**
* Set the locale code to use for formula translations and any special formatting.
@ -169,9 +174,19 @@ class Settings
/**
* Set the HTTP client implementation to be used for network request.
*/
public static function setHttpClient(ClientInterface $httpClient): void
public static function setHttpClient(ClientInterface $httpClient, RequestFactoryInterface $requestFactory): void
{
self::$client = $httpClient;
self::$httpClient = $httpClient;
self::$requestFactory = $requestFactory;
}
/**
* Unset the HTTP client configuration.
*/
public static function unsetHttpClient(): void
{
self::$httpClient = null;
self::$requestFactory = null;
}
/**
@ -179,10 +194,25 @@ class Settings
*/
public static function getHttpClient(): ClientInterface
{
if (!self::$client) {
self::$client = new Client();
self::assertHttpClient();
return self::$httpClient;
}
return self::$client;
/**
* Get the HTTP request factory.
*/
public static function getRequestFactory(): RequestFactoryInterface
{
self::assertHttpClient();
return self::$requestFactory;
}
private static function assertHttpClient(): void
{
if (!self::$httpClient || !self::$requestFactory) {
throw new Exception('HTTP client must be configured via Settings::setHttpClient() to be able to use WEBSERVICE function.');
}
}
}

View File

@ -2,47 +2,46 @@
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Web;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Web;
use PhpOffice\PhpSpreadsheet\Settings;
use PHPUnit\Framework\TestCase;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
class WebServiceTest extends TestCase
{
protected static $client;
public static function setUpBeforeClass(): void
protected function tearDown(): void
{
// Prevent URL requests being sent out
$mock = new MockHandler([
new ClientException('This is not a valid URL', new Request('GET', 'test'), new Response()),
new ConnectException('This is a 404 error', new Request('GET', 'test')),
new Response('200', [], str_repeat('a', 40000)),
new Response('200', [], 'This is a test'),
]);
$handlerStack = HandlerStack::create($mock);
self::$client = new Client(['handler' => $handlerStack]);
}
protected function setUp(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
Settings::unsetHttpClient();
}
/**
* @dataProvider providerWEBSERVICE
*/
public function testWEBSERVICE(string $expectedResult, string $url): void
public function testWEBSERVICE(string $expectedResult, string $url, ?array $responseData): void
{
Settings::setHttpClient(self::$client);
if ($responseData) {
$body = $this->createMock(StreamInterface::class);
$body->expects(self::atMost(1))->method('getContents')->willReturn($responseData[1]);
$response = $this->createMock(ResponseInterface::class);
$response->expects(self::once())->method('getStatusCode')->willReturn($responseData[0]);
$response->expects(self::atMost(1))->method('getBody')->willReturn($body);
$client = $this->createMock(ClientInterface::class);
$client->expects(self::once())->method('sendRequest')->willReturn($response);
$request = $this->createMock(RequestInterface::class);
$requestFactory = $this->createMock(RequestFactoryInterface::class);
$requestFactory->expects(self::atMost(1))->method('createRequest')->willReturn($request);
Settings::setHttpClient($client, $requestFactory);
}
$result = Web::WEBSERVICE($url);
self::assertEquals($expectedResult, $result);
}
@ -51,4 +50,28 @@ class WebServiceTest extends TestCase
{
return require 'tests/data/Calculation/Web/WEBSERVICE.php';
}
public function testWEBSERVICEReturnErrorWhenClientThrows(): void
{
$exception = $this->createMock(\Psr\Http\Client\ClientExceptionInterface::class);
$client = $this->createMock(ClientInterface::class);
$client->expects(self::once())->method('sendRequest')->willThrowException($exception);
$request = $this->createMock(RequestInterface::class);
$requestFactory = $this->createMock(RequestFactoryInterface::class);
$requestFactory->expects(self::atMost(1))->method('createRequest')->willReturn($request);
Settings::setHttpClient($client, $requestFactory);
$result = Web::WEBSERVICE('https://example.com');
self::assertEquals('#VALUE!', $result);
}
public function testWEBSERVICEThrowsIfNotClientConfigured(): void
{
$this->expectExceptionMessage('HTTP client must be configured via Settings::setHttpClient() to be able to use WEBSERVICE function.');
Web::WEBSERVICE('https://example.com');
}
}

View File

@ -4,25 +4,27 @@ return [
[
'#VALUE!',
'http://www.thisurlisfartoolongLoremipsumdolorsitametconsecteturadipiscingelitAliquamimperdietmetusurnasedaliquampurusdapibusefficiturQuisqueatullamcorpermaurisacmattisanteDonecsagittisauguenullaegeinterduurnapharetrautQuisquealectusvelnisivolutpatpharetraSuspendisseconvallisvulputateblanditClassaptenttacitisociosquadlitoratorquentperconubianostraperinceptoshimenaeosProinjustdiampulvinaracjustoauctorimperdietsuscipitestEtiamacmaximusmassasitametvulputatedolorthisurlisfartoolongLoremipsumdolorsitametconsecteturadipiscingelitAliquamimperdietmetusurnasedaliquampurusdapibusefficiturQuisqueatullamcorpermaurisacmattisanteDonecsagittisauguenullaegeinterduurnapharetrautQuisquealectusvelnisivolutpatpharetraSuspendisseconvallisvulputateblanditClassaptenttacitisociosquadlitoratorquentperconubianostraperinceptoshimenaeosProinjustdiampulvinaracjustoauctorimperdietsuscipitestEtiamacmaximusmassasitametvulputatedolorthisurlisfartoolongLoremipsumdolorsitametconsecteturadipiscingelitAliquamimperdietmetusurnasedaliquampurusdapibusefficiturQuisqueatullamcorpermaurisacmattisanteDonecsagittisauguenullaegeinterduurnapharetrautQuisquealectusvelnisivolutpatpharetraSuspendisseconvallisvulputateblanditClassaptenttacitisociosquadlitoratorquentperconubianostraperinceptoshimenaeosProinjustdiampulvinaracjustoauctorimperdietsuscipitestEtiamacmaximusmassasitametvulputatedolorthisurlisfartoolongLoremipsumdolorsitametconsecteturadipiscingelitAliquamimperdietmetusurnasedaliquampurusdapibusefficiturQuisqueatullamcorpermaurisacmattisanteDonecsagittisauguenullaegeinterduurnapharetrautQuisquealectusvelnisivolutpatpharetraSuspendisseconvallisvulputateblanditClassaptenttacitisociosquadlitoratorquentperconubianostraperinceptoshimenaeosProinjustdiampulvinaracjustoauctorimperdietsuscipitestEtiamacmaximusmassasitametvulputatedolorthisurlisfartoolongLoremipsumdolorsitametconsecteturadipiscingelitAliquamimperdietmetusurnasedaliquampurusdapibusefficiturQuisqueatullamcorpermaurisacmattisanteDonecsagittisauguenullaegeinterduurnapharetrautQuisquealectusvelnisivolutpatpharetraSuspendisseconvallisvulputateblanditClassaptenttacitisociosquadlitoratorquentperconubianostraperinceptoshimenaeosProinjustdiampulvinaracjustoauctorimperdietsuscipitestEtiamacmaximusmassasitametvulputatedolorthisurlisfartoolongLoremipsumdolorsitametconsecteturadipiscingelitAliquamimperdietmetusurnasedaliquampurusdapibusefficiturQuisqueatullamcorpermaurisacmattisanteDonecsagittisauguenullaegeinterduurnapharetrautQuisquealectusvelnisivolutpatpharetraSuspendisseconvallisvulputateblanditClassaptenttacitisociosquadlitoratorquentperconubianostraperinceptoshimenaeosProinjustdiampulvinaracjustoauctorimperdietsuscipitestEtiamacmaximusmassasitametvulputatedolor.com',
null,
],
[
'#VALUE!',
'ftp://www.bla.com',
],
[
'#VALUE!',
'http://notevenanurl',
null,
],
[
'#VALUE!',
'http://www.example1.com',
['404', 'not found'],
],
[
'#VALUE!',
'http://www.example2.com',
['200', str_repeat('a', 40000)],
],
[
'This is a test',
'http://www.example3.com',
['200', 'This is a test'],
],
];