From 70a90ff4612c4b55c175a32ffae1fb08f85e5765 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 12 Jul 2026 02:33:06 +0100 Subject: [PATCH] Add support for Guzzle 8 --- composer.json | 4 +- src/WebPush.php | 18 +++++---- tests/WebPushRejectedReportTest.php | 58 +++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 tests/WebPushRejectedReportTest.php diff --git a/composer.json b/composer.json index 4cbdfa8..a1c0803 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,9 @@ "ext-json": "*", "ext-mbstring": "*", "ext-openssl": "*", - "guzzlehttp/guzzle": "^7.9.2", + "guzzlehttp/guzzle": "^7.9.2|^8.0", + "guzzlehttp/psr7": "^2.7|^3.0", + "psr/http-client": "^1.0", "psr/log": "^2.0|^3.0", "spomky-labs/base64url": "^2.0.4", "symfony/polyfill-php83": "^1.33", diff --git a/src/WebPush.php b/src/WebPush.php index 0fdb794..31dabbf 100644 --- a/src/WebPush.php +++ b/src/WebPush.php @@ -13,9 +13,9 @@ use Base64Url\Base64Url; use GuzzleHttp\Client; use GuzzleHttp\Pool; -use GuzzleHttp\Exception\ConnectException; -use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7\Request; +use Psr\Http\Client\NetworkExceptionInterface; +use Psr\Http\Client\RequestExceptionInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; @@ -206,7 +206,7 @@ public function flushPooled(callable $callback, ?int $batchSize = null, ?int $re $batch = $this->prepare($batch); $pool = new Pool($this->client, $batch, [ 'concurrency' => $requestConcurrency, - 'fulfilled' => function (ResponseInterface $response, int $index) use ($callback, $batch): void { + 'fulfilled' => function (ResponseInterface $response, int|string $index) use ($callback, $batch): void { /** @var RequestInterface $request **/ $request = $batch[$index]; $callback(new MessageSentReport($request, $response)); @@ -225,11 +225,13 @@ public function flushPooled(callable $callback, ?int $batchSize = null, ?int $re } } - protected function createRejectedReport(RequestException|ConnectException $reason): MessageSentReport + protected function createRejectedReport(NetworkExceptionInterface|RequestExceptionInterface $reason): MessageSentReport { - if ($reason instanceof RequestException) { - $response = $reason->getResponse(); - } else { + // Guzzle 7's RequestException and Guzzle 8's ResponseException expose the + // response; Guzzle 8's RequestException and the network exceptions do not. + $response = is_callable([$reason, 'getResponse']) ? $reason->getResponse() : null; + + if (!$response instanceof ResponseInterface) { $response = null; } @@ -286,7 +288,7 @@ protected function prepare(array $notifications): array $content = ''; } - $headers['TTL'] = $options['TTL']; + $headers['TTL'] = (string) $options['TTL']; if (isset($options['urgency'])) { $headers['Urgency'] = $options['urgency']; diff --git a/tests/WebPushRejectedReportTest.php b/tests/WebPushRejectedReportTest.php new file mode 100644 index 0000000..11d1f8d --- /dev/null +++ b/tests/WebPushRejectedReportTest.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use GuzzleHttp\Exception\ConnectException; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Response; +use Minishlink\WebPush\Subscription; +use Minishlink\WebPush\WebPush; +use PHPUnit\Framework\Attributes\CoversClass; + +#[CoversClass(WebPush::class)] +final class WebPushRejectedReportTest extends PHPUnit\Framework\TestCase +{ + /** + * @throws \ErrorException + */ + public function testNetworkFailureProducesRejectedReport(): void + { + $handler = new MockHandler([ + new ConnectException('Connection refused', new Request('POST', 'https://push.example.com/send')), + ]); + $webPush = new WebPush([], [], 30, ['handler' => HandlerStack::create($handler)]); + + $report = $webPush->sendOneNotification(new Subscription('https://push.example.com/send')); + + $this->assertFalse($report->isSuccess()); + $this->assertNull($report->getResponse()); + $this->assertEquals('Connection refused', $report->getReason()); + $this->assertEquals('https://push.example.com/send', $report->getEndpoint()); + } + + /** + * @throws \ErrorException + */ + public function testHttpErrorProducesRejectedReportWithResponse(): void + { + $handler = new MockHandler([ + new Response(410, [], '', '1.1', 'Gone'), + ]); + $webPush = new WebPush([], [], 30, ['handler' => HandlerStack::create($handler)]); + + $report = $webPush->sendOneNotification(new Subscription('https://push.example.com/send')); + + $this->assertFalse($report->isSuccess()); + $this->assertTrue($report->isSubscriptionExpired()); + $this->assertEquals(410, $report->getResponse()->getStatusCode()); + $this->assertNotEmpty($report->getReason()); + } +}