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';