diff --git a/composer.json b/composer.json index a2a76015..d31ab741 100755 --- a/composer.json +++ b/composer.json @@ -5,8 +5,8 @@ "homepage": "https://github.com/zendesk/zendesk_api_client_php", "require": { "php": ">=8.2.0", - "guzzlehttp/guzzle": "^6.0 || ^7.0", - "guzzlehttp/psr7": "^1.7 || ^2.0", + "guzzlehttp/guzzle": "^7.8.2 || ^8.0", + "guzzlehttp/psr7": "^2.6.3 || ^3.0", "mmucklo/inflect": "0.3.*" }, "require-dev": { diff --git a/composer.lock b/composer.lock index ccb6e7b8..df39b41c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,29 +4,30 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "98b537b2c22e2f67b8facecd331c30e4", + "content-hash": "3350359cad4d86d88b2ec132d46c59f1", "packages": [ { "name": "guzzlehttp/guzzle", - "version": "7.9.2", + "version": "7.14.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "aef242412e13128b5049864867bb49fc37dd39de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aef242412e13128b5049864867bb49fc37dd39de", + "reference": "aef242412e13128b5049864867bb49fc37dd39de", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.5.1", + "guzzlehttp/psr7": "^2.12.4", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-client-implementation": "1.0" @@ -34,9 +35,10 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "guzzle/client-integration-tests": "3.0.2", + "guzzle/client-integration-tests": "3.0.3", + "guzzlehttp/test-server": "^0.6", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -114,7 +116,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + "source": "https://github.com/guzzle/guzzle/tree/7.14.0" }, "funding": [ { @@ -130,28 +132,29 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2026-07-08T22:54:09+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.4", + "version": "2.5.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "url": "https://api.github.com/repos/guzzle/promises/zipball/9ad1e4fc607446a055b95870c7f668e93b5cff29", + "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0" + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "type": "library", "extra": { @@ -197,7 +200,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" + "source": "https://github.com/guzzle/promises/tree/2.5.1" }, "funding": [ { @@ -213,27 +216,29 @@ "type": "tidelift" } ], - "time": "2024-10-17T10:06:22+00:00" + "time": "2026-07-08T15:48:39+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.0", + "version": "2.12.4", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + "reference": "51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c", + "reference": "51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -241,8 +246,9 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -313,7 +319,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" + "source": "https://github.com/guzzle/psr7/tree/2.12.4" }, "funding": [ { @@ -329,7 +335,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T11:15:46+00:00" + "time": "2026-07-08T15:56:20+00:00" }, { "name": "mmucklo/inflect", @@ -591,16 +597,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -608,12 +614,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" } }, "autoload": { @@ -638,7 +644,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -649,12 +655,100 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" } ], "packages-dev": [ @@ -3933,5 +4027,5 @@ "php": ">=8.2.0" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/src/Zendesk/API/Exceptions/ApiResponseException.php b/src/Zendesk/API/Exceptions/ApiResponseException.php index 2eb0d3c8..4d45139d 100644 --- a/src/Zendesk/API/Exceptions/ApiResponseException.php +++ b/src/Zendesk/API/Exceptions/ApiResponseException.php @@ -4,6 +4,7 @@ use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Exception\ResponseException; use GuzzleHttp\Exception\ServerException; /** @@ -22,6 +23,9 @@ public function __construct(RequestException $e) { $message = $e->getMessage(); + $hasResponse = $e instanceof ResponseException + || (is_callable([$e, 'hasResponse']) && $e->hasResponse()); + if ($e instanceof ClientException) { $response = $e->getResponse(); $responseBody = $response->getBody()->getContents(); @@ -29,7 +33,7 @@ public function __construct(RequestException $e) $message .= ' [details] ' . $this->errorDetails; } elseif ($e instanceof ServerException) { $message .= ' [details] Zendesk may be experiencing internal issues or undergoing scheduled maintenance.'; - } elseif (! $e->hasResponse()) { + } elseif (! $hasResponse) { $request = $e->getRequest(); // Unsuccessful response, log what we can $message .= ' [url] ' . $request->getUri(); diff --git a/src/Zendesk/API/Http.php b/src/Zendesk/API/Http.php index 14fba49f..986b3d63 100644 --- a/src/Zendesk/API/Http.php +++ b/src/Zendesk/API/Http.php @@ -3,9 +3,11 @@ namespace Zendesk\API; use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Exception\ResponseException; use GuzzleHttp\Psr7\LazyOpenStream; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Utils; +use Psr\Http\Client\NetworkExceptionInterface; use Psr\Http\Message\StreamInterface; use Zendesk\API\Exceptions\ApiResponseException; use Zendesk\API\Exceptions\AuthException; @@ -95,8 +97,13 @@ public static function send( } $response = $client->guzzle->send($request, $requestOptions); } catch (RequestException $e) { - $requestException = RequestException::create($e->getRequest(), $e->getResponse(), $e); + $errorResponse = $e instanceof ResponseException + ? $e->getResponse() + : (is_callable([$e, 'getResponse']) ? $e->getResponse() : null); + $requestException = RequestException::create($e->getRequest(), $errorResponse, $e); throw new ApiResponseException($requestException); + } catch (NetworkExceptionInterface $e) { + throw new ApiResponseException(RequestException::create($e->getRequest(), null, $e)); } finally { $client->setDebug( $request->getHeaders(), diff --git a/src/Zendesk/API/Traits/Resource/MultipartUpload.php b/src/Zendesk/API/Traits/Resource/MultipartUpload.php index 3dda0380..5f946915 100644 --- a/src/Zendesk/API/Traits/Resource/MultipartUpload.php +++ b/src/Zendesk/API/Traits/Resource/MultipartUpload.php @@ -68,18 +68,21 @@ public function upload(array $params, $routeKey = __FUNCTION__) $stream = new LazyOpenStream($params['file'], 'r'); } + $part = [ + 'name' => $this->getUploadName(), + 'contents' => $stream + ]; + + if (is_string($filename)) { + $part['filename'] = $filename; + } + $response = Http::send( $this->client, $route, [ 'method' => $this->getUploadRequestMethod(), - 'multipart' => [ - [ - 'name' => $this->getUploadName(), - 'contents' => $stream, - 'filename' => $filename - ] - ] + 'multipart' => [$part] ] ); diff --git a/src/Zendesk/Fixtures/MockResource.php b/src/Zendesk/Fixtures/MockResource.php index 51dbcc87..303eac6a 100644 --- a/src/Zendesk/Fixtures/MockResource.php +++ b/src/Zendesk/Fixtures/MockResource.php @@ -3,6 +3,7 @@ namespace Zendesk\Fixtures; use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Exception\ResponseException; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use Zendesk\API\Exceptions\ApiResponseException; @@ -30,7 +31,11 @@ public function findAll($params) if ($this->errorMessage) { $request = new Request('GET', 'http://example.zendesk.com'); $this->response = new Response(400, [], '{ "a": "json"}'); - $requestException = new RequestException($this->errorMessage, $request, $this->response); + if (class_exists(ResponseException::class)) { + $requestException = new ResponseException($this->errorMessage, $request, $this->response); + } else { + $requestException = new RequestException($this->errorMessage, $request, $this->response); + } throw new ApiResponseException($requestException); } elseif ($this->isObp) { $this->response = (object) [ diff --git a/tests/Zendesk/API/UnitTests/Core/ResourceTest.php b/tests/Zendesk/API/UnitTests/Core/ResourceTest.php index c5a1ffb2..266416cb 100644 --- a/tests/Zendesk/API/UnitTests/Core/ResourceTest.php +++ b/tests/Zendesk/API/UnitTests/Core/ResourceTest.php @@ -3,9 +3,11 @@ namespace Zendesk\API\UnitTests\Core; use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7\AppendStream; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\LazyOpenStream; +use GuzzleHttp\Psr7\Utils; use Zendesk\API\UnitTests\BasicTest; /** @@ -378,4 +380,28 @@ public function testUploadStreamMultiPart() ] ); } + + /** + * Test multipart upload with streams that have no file uri + */ + public function testUploadStreamWithoutUriMultiPart() + { + $this->mockAPIResponses([ + new Response(200, [], '') + ]); + + $params = [ + 'file' => new AppendStream([Utils::streamFor('test file contents')]) + ]; + + $this->dummyResource->upload($params); + + $this->assertLastRequestIs( + [ + 'method' => 'POST', + 'endpoint' => 'dummy_resource/uploads.json', + 'multipart' => true, + ] + ); + } } diff --git a/tests/Zendesk/API/UnitTests/HttpTest.php b/tests/Zendesk/API/UnitTests/HttpTest.php index 0b5ba4b6..e13b9eb8 100644 --- a/tests/Zendesk/API/UnitTests/HttpTest.php +++ b/tests/Zendesk/API/UnitTests/HttpTest.php @@ -5,10 +5,13 @@ use Exception; use Faker\Factory; use GuzzleHttp\Client as GuzzleClient; +use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Exception\ResponseException; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Stream; +use Zendesk\API\Exceptions\ApiResponseException; use Zendesk\API\Http; use Zendesk\API\HttpClient; @@ -56,6 +59,20 @@ public function testOriginalRequestExceptionIsPreserved() } } + public function testConnectExceptionIsWrappedInApiResponseException() + { + $request = new Request('GET', 'http://example.com'); + $connectException = new ConnectException('Connection refused', $request); + $this->mockApiResponses([$connectException]); + + try { + Http::send($this->client, '/tickets.json'); + $this->fail('Expected ApiResponseException was not thrown.'); + } catch (ApiResponseException $e) { + $this->assertSame($connectException, $e->getPrevious()->getPrevious()); + } + } + /** * Create a mocked RequestExcpetion * @@ -79,6 +96,10 @@ private function mockRequestException($message) $response->method('getBody') ->will($this->returnValue($body)); + if (class_exists(ResponseException::class)) { + return new ResponseException($message, $request, $response); + } + return new RequestException($message, $request, $response); } } diff --git a/tests/Zendesk/API/UnitTests/Sell/ContactsTest.php b/tests/Zendesk/API/UnitTests/Sell/ContactsTest.php index 4d140571..1a911d2d 100644 --- a/tests/Zendesk/API/UnitTests/Sell/ContactsTest.php +++ b/tests/Zendesk/API/UnitTests/Sell/ContactsTest.php @@ -41,9 +41,9 @@ public function testUpsert() $encodedQueryParams = []; foreach ($queryParams as $key => $value) { - // Encode the 'phone' query param's whitespace + // Encode the 'phone' query param's whitespace and plus signs if ($key === 'phone') { - $value = str_replace(' ', '%20', $value); + $value = str_replace([' ', '+'], ['%20', '%2B'], $value); } $encodedQueryParams[$key] = $value; }