diff --git a/CHANGELOG.md b/CHANGELOG.md index 48ee6293..4d45ebac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/composer.json b/composer.json index d4810ce6..d0379949 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 99de82bf..266608f2 100644 --- a/composer.lock +++ b/composer.lock @@ -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" } diff --git a/docs/topics/settings.md b/docs/topics/settings.md index 4463ceeb..d28a9996 100644 --- a/docs/topics/settings.md +++ b/docs/topics/settings.md @@ -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); +``` diff --git a/src/PhpSpreadsheet/Calculation/Web.php b/src/PhpSpreadsheet/Calculation/Web.php index 9f0faf99..5cfd2ea8 100644 --- a/src/PhpSpreadsheet/Calculation/Web.php +++ b/src/PhpSpreadsheet/Calculation/Web.php @@ -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 } diff --git a/src/PhpSpreadsheet/Settings.php b/src/PhpSpreadsheet/Settings.php index 15218c72..cfa50573 100644 --- a/src/PhpSpreadsheet/Settings.php +++ b/src/PhpSpreadsheet/Settings.php @@ -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::$client; + return self::$httpClient; + } + + /** + * 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.'); + } } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Web/WebServiceTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Web/WebServiceTest.php index acc83cff..2aff7b3d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Web/WebServiceTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Web/WebServiceTest.php @@ -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'); + } } diff --git a/tests/data/Calculation/Web/WEBSERVICE.php b/tests/data/Calculation/Web/WEBSERVICE.php index 6d9934da..c8294eb2 100644 --- a/tests/data/Calculation/Web/WEBSERVICE.php +++ b/tests/data/Calculation/Web/WEBSERVICE.php @@ -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'], ], ];