diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f56754..f5801af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## Unreleased + +PHP 8.5 deprecation fixes for the 0.3 line. No API change. + +- `NativeSsrTransport` and `NativeSsrPurgeTransport` no longer call + `curl_close()` (deprecated in PHP 8.5, no effect since 8.0). It was logged + on every cURL render and purge. +- The streams fallback reads the response headers from + `stream_get_meta_data()`, not `$http_response_header` (deprecated in + PHP 8.5). +- Tests cover both transports over cURL and streams and fail on + deprecations. + ## 0.3.0 — 2026-09-12 First public Packagist release. Composer name is `mapsight/embed` (MIT). diff --git a/phpunit.xml b/phpunit.xml index 94d6eaa..f02c941 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,10 +3,13 @@ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" - cacheDirectory=".phpunit.cache"> + cacheDirectory=".phpunit.cache" + failOnDeprecation="true" + displayDetailsOnTestsThatTriggerDeprecations="true"> tests + tests/fixtures diff --git a/src/Embed/NativeSsrPurgeTransport.php b/src/Embed/NativeSsrPurgeTransport.php index cdc92df..782eee4 100644 --- a/src/Embed/NativeSsrPurgeTransport.php +++ b/src/Embed/NativeSsrPurgeTransport.php @@ -47,7 +47,7 @@ private function postWithCurl( $body = curl_exec($handle); $status = (int) curl_getinfo($handle, CURLINFO_HTTP_CODE); $error = curl_error($handle); - curl_close($handle); + unset($handle); if ($body === false) { throw new \RuntimeException($error !== '' ? $error : 'SSR purge request failed'); @@ -76,14 +76,27 @@ private function postWithStreams(string $url, string $json, float $timeoutSecond ], ]); - $body = @file_get_contents($url, false, $context); + // Response headers come from the stream metadata: PHP 8.5 deprecates + // $http_response_header, and http_get_last_response_headers() needs 8.4. + $stream = @fopen($url, 'rb', false, $context); + if ($stream === false) { + throw new \RuntimeException('SSR purge request failed'); + } + + try { + $headers = stream_get_meta_data($stream)['wrapper_data'] ?? null; + $body = stream_get_contents($stream); + } finally { + fclose($stream); + } + if ($body === false) { throw new \RuntimeException('SSR purge request failed'); } $status = 0; - if (isset($http_response_header[0]) - && preg_match('/\s(\d{3})\s/', $http_response_header[0], $matches) === 1 + if (is_array($headers) && is_string($headers[0] ?? null) + && preg_match('/\s(\d{3})\s/', $headers[0], $matches) === 1 ) { $status = (int) $matches[1]; } diff --git a/src/Embed/NativeSsrTransport.php b/src/Embed/NativeSsrTransport.php index 6b6c5c0..8fa8f7b 100644 --- a/src/Embed/NativeSsrTransport.php +++ b/src/Embed/NativeSsrTransport.php @@ -62,7 +62,7 @@ private function postWithCurl( $body = curl_exec($handle); $status = (int) curl_getinfo($handle, CURLINFO_HTTP_CODE); $error = curl_error($handle); - curl_close($handle); + unset($handle); if ($body === false) { throw new \RuntimeException($error !== '' ? $error : 'SSR request failed'); @@ -98,14 +98,27 @@ private function postWithStreams(string $url, string $json, float $timeoutSecond ], ]); - $body = @file_get_contents($url, false, $context); + // Response headers come from the stream metadata: PHP 8.5 deprecates + // $http_response_header, and http_get_last_response_headers() needs 8.4. + $stream = @fopen($url, 'rb', false, $context); + if ($stream === false) { + throw new \RuntimeException('SSR request failed'); + } + + try { + $headers = stream_get_meta_data($stream)['wrapper_data'] ?? null; + $body = stream_get_contents($stream); + } finally { + fclose($stream); + } + if ($body === false) { throw new \RuntimeException('SSR request failed'); } $status = 0; - if (isset($http_response_header[0]) - && preg_match('/\s(\d{3})\s/', $http_response_header[0], $matches) === 1 + if (is_array($headers) && is_string($headers[0] ?? null) + && preg_match('/\s(\d{3})\s/', $headers[0], $matches) === 1 ) { $status = (int) $matches[1]; } diff --git a/tests/NativeTransportsTest.php b/tests/NativeTransportsTest.php new file mode 100644 index 0000000..f3c8562 --- /dev/null +++ b/tests/NativeTransportsTest.php @@ -0,0 +1,109 @@ + ['file', '/dev/null', 'r'], + 1 => ['file', '/dev/null', 'w'], + 2 => ['file', '/dev/null', 'w'], + ], + $pipes, + ); + if (self::$process === false) { + self::markTestSkipped('could not start php -S'); + } + + for ($i = 0; $i < 50; $i++) { + $fp = @fsockopen('127.0.0.1', self::$port, $errno, $errstr, 0.1); + if (is_resource($fp)) { + fclose($fp); + + return; + } + usleep(50000); + } + + self::tearDownAfterClass(); + self::markTestSkipped('php -S did not become ready'); + } + + public static function tearDownAfterClass(): void + { + if (self::$process !== false) { + proc_terminate(self::$process); + proc_close(self::$process); + self::$process = false; + } + } + + /** @return iterable */ + public static function paths(): iterable + { + yield 'curl' => ['postWithCurl']; + yield 'streams' => ['postWithStreams']; + } + + #[DataProvider('paths')] + public function test_render_reads_body_on_2xx(string $method): void + { + $body = $this->render($method, '/render', ['X-Request-Id' => 'req-1']); + + $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + $this->assertSame(1, $data['v'] ?? null); + $this->assertSame('req-1', $data['state']['requestId'] ?? null); + } + + #[DataProvider('paths')] + public function test_render_throws_on_5xx(string $method): void + { + $this->expectExceptionMessage('SSR HTTP status 503'); + $this->render($method, '/render?status=503'); + } + + #[DataProvider('paths')] + public function test_purge_reads_status(string $method): void + { + $this->assertSame('["doc::1"]', $this->purge($method, '/purge')); + $this->assertSame('[]', $this->purge($method, '/purge?status=204')); + } + + /** @param array $headers */ + private function render(string $method, string $path, array $headers = []): string + { + $transport = new NativeSsrTransport(); + $url = 'http://127.0.0.1:' . self::$port . $path; + $args = $method === 'postWithCurl' + ? [$url, '{"v":1}', 2.0, 0.5, $headers] + : [$url, '{"v":1}', 2.0, $headers]; + + return (new \ReflectionMethod($transport, $method))->invoke($transport, ...$args); + } + + private function purge(string $method, string $path): string + { + $transport = new NativeSsrPurgeTransport(); + $url = 'http://127.0.0.1:' . self::$port . $path; + $args = $method === 'postWithCurl' ? [$url, '{}', 2.0, 0.5] : [$url, '{}', 2.0]; + + return (new \ReflectionMethod($transport, $method))->invoke($transport, ...$args); + } +} diff --git a/tests/fixtures/http-server.php b/tests/fixtures/http-server.php new file mode 100644 index 0000000..e35978d --- /dev/null +++ b/tests/fixtures/http-server.php @@ -0,0 +1,41 @@ + 1, + 'html' => '
', + 'state' => ['requestId' => $_SERVER['HTTP_X_REQUEST_ID'] ?? null], + ], JSON_THROW_ON_ERROR); + exit; +} + +if ($path === '/purge') { + if (($query['status'] ?? '') === '204') { + http_response_code(204); + exit; + } + + http_response_code(200); + header('Content-Type: application/json'); + echo '["doc::1"]'; + exit; +} + +http_response_code(404); +header('Content-Type: text/plain'); +echo 'not found';