From 68480827b7a5384dd1b1235ac3589406f3ba5c34 Mon Sep 17 00:00:00 2001 From: "Beau Beauchamp, WebTigers" Date: Sat, 29 Aug 2026 02:54:44 -0400 Subject: [PATCH] fix(php85): eliminate every deprecation, and fail the build on a new one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suite reported 15 deprecations on PHP 8.5. They were tolerated rather than fixed, which meant new ones could accumulate unnoticed — and two of these classes are not cosmetic: implicitly nullable parameters become a FATAL in PHP 9. Fixed at the source, never suppressed: - **Implicit nullable params** — `array $Block = null` -> `?array $Block = null` in the vendored Parsedown (2 signatures). `?array` is exactly what the implicit form already meant, so behaviour is identical. Parsedown is third-party, so the change is documented in a LOCAL MODIFICATIONS header block; re-apply it if the library is ever re-vendored. The matching Zend_View one shipped separately as TigerZF v1.32.2 (the floor here moves to ^1.32.2 so consumers actually get it). - **`curl_close()`** — a no-op since PHP 8.0 (the handle is an object freed by refcount), deprecated in 8.5. Removed at ALL 12 call sites, not only the one the tests happened to reach: Recaptcha, Google/Analytics (x2), Location/Adapter/Aws, Agent/Provider/{Gemini,OpenAiCompatible, Anthropic} (x2 each), register/Registration, and bin/mcp-bridge. Every one read curl_getinfo / curl_error BEFORE the call, so removal is behaviour-preserving. - **`imagedestroy()`** — same story (GdImage is an object since 8.0). Removed from Media/Image, with a note that the memory is released at exactly the points it always was: `$dst` when the next loop iteration reassigns it, `$src` when the scope ends. - **`ReflectionProperty/Method::setAccessible()`** — a no-op since PHP 8.1. Removed from 9 test sites AND from Application/Bootstrap, which the suite never exercised — most of the codebase had already been cleaned, these were the stragglers. Then the ratchet: `failOnDeprecation="true"` in phpunit.xml, so a NEW deprecation fails the build by exit code instead of being reported and ignored. Verified both ways — reintroducing the Parsedown signature makes the run exit 1, restoring it exits 0. Full suite: OK (2111 tests, 21490 assertions) — clean green, zero deprecations. --- bin/mcp-bridge.php | 1 - composer.json | 2 +- library/Tiger/Agent/Provider/Anthropic.php | 2 -- library/Tiger/Agent/Provider/Gemini.php | 2 -- .../Tiger/Agent/Provider/OpenAiCompatible.php | 2 -- library/Tiger/Application/Bootstrap.php | 5 +++-- library/Tiger/Cms/vendor/Parsedown.php | 18 ++++++++++++++++-- library/Tiger/Google/Analytics.php | 4 ++-- library/Tiger/Location/Adapter/Aws.php | 1 - library/Tiger/Media/Image.php | 5 +++-- library/Tiger/Recaptcha.php | 1 - modules/register/services/Registration.php | 1 - phpunit.xml | 2 ++ tests/Integration/Ally/BootstrapTest.php | 2 -- tests/Integration/Ally/ScanServiceTest.php | 1 - tests/Integration/Seo/BootstrapTest.php | 2 -- tests/Integration/Seo/ControllersTest.php | 2 -- .../Integration/Seo/SchemaServiceExtraTest.php | 1 - tests/Integration/Seo/SchemaServiceTest.php | 1 - 19 files changed, 27 insertions(+), 28 deletions(-) diff --git a/bin/mcp-bridge.php b/bin/mcp-bridge.php index 25f5c491..e83f4adc 100644 --- a/bin/mcp-bridge.php +++ b/bin/mcp-bridge.php @@ -102,7 +102,6 @@ function mcp_bridge_post($url, array $headers, $payload) $body = curl_exec($ch); $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = (string) curl_error($ch); - curl_close($ch); return $body === false ? [false, 0, '', ($err ?: 'curl failed')] : [true, $status, (string) $body, '']; } diff --git a/composer.json b/composer.json index fbb8193c..edcec406 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "require": { "php": ">=8.1", "paragonie/sodium_compat": "^1.21 || ^2.0", - "webtigers/tigerzf": "^1.30" + "webtigers/tigerzf": "^1.32.2" }, "suggest": { "aws/aws-sdk-php": "S3 media storage (Tiger_Media_Storage_S3) + Rekognition moderation + CloudWatch logging", diff --git a/library/Tiger/Agent/Provider/Anthropic.php b/library/Tiger/Agent/Provider/Anthropic.php index a1fa5f1a..66522707 100644 --- a/library/Tiger/Agent/Provider/Anthropic.php +++ b/library/Tiger/Agent/Provider/Anthropic.php @@ -101,7 +101,6 @@ public function models($apiKey = '') ]); $raw = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); - curl_close($ch); $body = json_decode((string) $raw, true); if ($code === 200 && !empty($body['data'])) { $out = []; @@ -192,7 +191,6 @@ protected function _post(array $payload, $apiKey) $raw = curl_exec($ch); $err = curl_error($ch); $code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); - curl_close($ch); if ($raw === false) { throw new RuntimeException('Could not reach the AI provider: ' . $err); diff --git a/library/Tiger/Agent/Provider/Gemini.php b/library/Tiger/Agent/Provider/Gemini.php index 26e3b33c..92619d56 100644 --- a/library/Tiger/Agent/Provider/Gemini.php +++ b/library/Tiger/Agent/Provider/Gemini.php @@ -72,7 +72,6 @@ public function models($apiKey = '') ]); $raw = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); - curl_close($ch); $body = json_decode((string) $raw, true); if ($code === 200 && !empty($body['models'])) { $out = []; @@ -144,7 +143,6 @@ protected function _post($url, array $payload, $apiKey) $raw = curl_exec($ch); $err = curl_error($ch); $code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); - curl_close($ch); if ($raw === false) { throw new RuntimeException('Could not reach the AI provider: ' . $err); diff --git a/library/Tiger/Agent/Provider/OpenAiCompatible.php b/library/Tiger/Agent/Provider/OpenAiCompatible.php index 56323327..33575150 100644 --- a/library/Tiger/Agent/Provider/OpenAiCompatible.php +++ b/library/Tiger/Agent/Provider/OpenAiCompatible.php @@ -108,7 +108,6 @@ public function models($apiKey = '') ]); $raw = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); - curl_close($ch); $body = json_decode((string) $raw, true); if ($code === 200 && !empty($body['data'])) { $out = []; @@ -151,7 +150,6 @@ protected function _post($url, array $payload, array $headers) $raw = curl_exec($ch); $err = curl_error($ch); $code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); - curl_close($ch); if ($raw === false) { throw new RuntimeException('Could not reach the AI provider: ' . $err); diff --git a/library/Tiger/Application/Bootstrap.php b/library/Tiger/Application/Bootstrap.php index edb4d13f..bd56bce3 100644 --- a/library/Tiger/Application/Bootstrap.php +++ b/library/Tiger/Application/Bootstrap.php @@ -371,8 +371,9 @@ protected function _registerCustomLocales(array $locales) return; } try { - $ref = new ReflectionProperty('Zend_Locale', '_localeData'); - $ref->setAccessible(true); + // No setAccessible(): PHP 8.1+ reflection reaches a private static directly, and the call + // is deprecated in 8.5. + $ref = new ReflectionProperty('Zend_Locale', '_localeData'); $data = $ref->getValue(); $added = false; foreach ($unknown as $l) { diff --git a/library/Tiger/Cms/vendor/Parsedown.php b/library/Tiger/Cms/vendor/Parsedown.php index 1b9d6d5b..37e04929 100644 --- a/library/Tiger/Cms/vendor/Parsedown.php +++ b/library/Tiger/Cms/vendor/Parsedown.php @@ -11,6 +11,20 @@ # For the full license information, view the LICENSE file that was distributed # with this source code. # +# --------------------------------------------------------------------------- +# LOCAL MODIFICATIONS (WebTigers) — keep these when re-vendoring. +# +# Upstream 1.7.4 is from 2019 and predates PHP 8.4's deprecation of implicitly +# nullable parameters. Two signatures declared `array $Block = null`, which PHP +# 8.4+ reports as deprecated on every markdown render: +# +# blockSetextHeader() ~line 715 array $Block = null -> ?array $Block = null +# blockTable() ~line 853 array $Block = null -> ?array $Block = null +# +# The types are otherwise unchanged and the behaviour is identical — `?array` is +# what the implicit form already meant. If you pull a newer Parsedown, re-apply +# these (or drop them if upstream has fixed it). +# --------------------------------------------------------------------------- # class Parsedown @@ -712,7 +726,7 @@ protected function blockRule($Line) # # Setext - protected function blockSetextHeader($Line, array $Block = null) + protected function blockSetextHeader($Line, ?array $Block = null) { if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) { @@ -850,7 +864,7 @@ protected function blockReference($Line) # # Table - protected function blockTable($Line, array $Block = null) + protected function blockTable($Line, ?array $Block = null) { if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) { diff --git a/library/Tiger/Google/Analytics.php b/library/Tiger/Google/Analytics.php index 7b5c905e..ff97ef63 100644 --- a/library/Tiger/Google/Analytics.php +++ b/library/Tiger/Google/Analytics.php @@ -493,7 +493,8 @@ private static function _http($url, array $opts) ]); $body = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); + // No curl_close(): since PHP 8.0 the handle is an object freed by refcount, so the call has + // been a no-op, and 8.5 deprecates it. Letting $ch fall out of scope is the close. return ($body !== false && $code >= 200 && $code < 300) ? $body : null; } @@ -511,7 +512,6 @@ private static function _probeReport($token) ]); $body = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); return [$code, $body === false ? '' : (string) $body]; } diff --git a/library/Tiger/Location/Adapter/Aws.php b/library/Tiger/Location/Adapter/Aws.php index 491fedb9..882a3651 100644 --- a/library/Tiger/Location/Adapter/Aws.php +++ b/library/Tiger/Location/Adapter/Aws.php @@ -210,7 +210,6 @@ protected function _signedPost( ]); $respBody = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); if ($code < 200 || $code >= 300 || $respBody === false) { return null; diff --git a/library/Tiger/Media/Image.php b/library/Tiger/Media/Image.php index c20a5d59..bb6c08c1 100644 --- a/library/Tiger/Media/Image.php +++ b/library/Tiger/Media/Image.php @@ -89,9 +89,10 @@ public static function variants($sourcePath, $mime, array $presets, $quality = 9 if ($tmp !== false && self::_save($dst, $mime, $tmp, $quality)) { $out[$name] = ['path' => $tmp, 'width' => $nw, 'height' => $nh, 'mime' => (string) $mime]; } - imagedestroy($dst); + // No imagedestroy(): since PHP 8.0 a GD handle is a GdImage OBJECT freed by refcount, so + // the call has been a no-op (8.5 deprecates it). The memory is released at the same points + // it always was — $dst when the next iteration reassigns it, $src when this scope ends. } - imagedestroy($src); return $out; } diff --git a/library/Tiger/Recaptcha.php b/library/Tiger/Recaptcha.php index 168c63ce..120f70b0 100644 --- a/library/Tiger/Recaptcha.php +++ b/library/Tiger/Recaptcha.php @@ -141,7 +141,6 @@ protected static function _post($url, array $params) ]); $body = curl_exec($ch); $ok = ($body !== false && curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 200 && curl_getinfo($ch, CURLINFO_HTTP_CODE) < 300); - curl_close($ch); return $ok ? $body : null; } diff --git a/modules/register/services/Registration.php b/modules/register/services/Registration.php index a33e682f..bd203a87 100644 --- a/modules/register/services/Registration.php +++ b/modules/register/services/Registration.php @@ -169,7 +169,6 @@ private function _registry(string $service, string $method, array $params): ?arr ]); $body = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); if ($body === false || $code !== 200) { return null; } $j = json_decode((string) $body, true); if (!is_array($j) || (int) ($j['result'] ?? 0) !== 1) { return null; } diff --git a/phpunit.xml b/phpunit.xml index 1f223e01..7caee9cd 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -13,9 +13,11 @@ colors="true" failOnRisky="true" failOnWarning="true" + failOnDeprecation="true" beStrictAboutOutputDuringTests="true" beStrictAboutTestsThatDoNotTestAnything="true" displayDetailsOnTestsThatTriggerWarnings="true" + displayDetailsOnTestsThatTriggerDeprecations="true" displayDetailsOnTestsThatTriggerErrors="true"> diff --git a/tests/Integration/Ally/BootstrapTest.php b/tests/Integration/Ally/BootstrapTest.php index bbbeb30d..b9c613f8 100644 --- a/tests/Integration/Ally/BootstrapTest.php +++ b/tests/Integration/Ally/BootstrapTest.php @@ -39,7 +39,6 @@ protected function tearDown(): void private function registered(): array { $p = new ReflectionProperty(Tiger_Admin_Nav::class, '_items'); - $p->setAccessible(true); $items = []; foreach ((array) $p->getValue() as $item) { $items[$item['key'] ?? ''] = $item; @@ -54,7 +53,6 @@ public function it_registers_the_accessibility_sidebar_item(): void $bootstrap = (new \ReflectionClass(Ally_Bootstrap::class))->newInstanceWithoutConstructor(); $m = new ReflectionMethod(Ally_Bootstrap::class, '_initAdminNav'); - $m->setAccessible(true); $m->invoke($bootstrap); $items = $this->registered(); diff --git a/tests/Integration/Ally/ScanServiceTest.php b/tests/Integration/Ally/ScanServiceTest.php index 73eb71aa..2020a127 100644 --- a/tests/Integration/Ally/ScanServiceTest.php +++ b/tests/Integration/Ally/ScanServiceTest.php @@ -209,7 +209,6 @@ public function scan_reports_a_render_failure_for_a_broken_page(): void // buffering) — swap Tiger_Log's logger for a null writer so the diagnostic line doesn't count as // unexpected test output under strict mode. Restored after. $logProp = new \ReflectionProperty(\Tiger_Log::class, '_log'); - $logProp->setAccessible(true); $prior = $logProp->getValue(); $logProp->setValue(null, (new \Zend_Log())->addWriter(new \Zend_Log_Writer_Null())); try { diff --git a/tests/Integration/Seo/BootstrapTest.php b/tests/Integration/Seo/BootstrapTest.php index 2b554f28..c9daa540 100644 --- a/tests/Integration/Seo/BootstrapTest.php +++ b/tests/Integration/Seo/BootstrapTest.php @@ -44,14 +44,12 @@ private function bootstrap(): Seo_Bootstrap private function invoke(string $method): void { $m = new ReflectionMethod(Seo_Bootstrap::class, $method); - $m->setAccessible(true); $m->invoke($this->bootstrap()); } private function resetProviders(): void { $p = new ReflectionProperty(Tiger_Sitemap::class, '_providers'); - $p->setAccessible(true); $p->setValue(null, []); } diff --git a/tests/Integration/Seo/ControllersTest.php b/tests/Integration/Seo/ControllersTest.php index e6d2ce24..942b86f8 100644 --- a/tests/Integration/Seo/ControllersTest.php +++ b/tests/Integration/Seo/ControllersTest.php @@ -77,14 +77,12 @@ protected function tearDown(): void private function resetProviders(): void { $p = new ReflectionProperty(Tiger_Sitemap::class, '_providers'); - $p->setAccessible(true); $p->setValue(null, []); } private function resetSiteOrg(): void { $p = new ReflectionProperty(\Tiger_Model_Org::class, '_siteOrgId'); - $p->setAccessible(true); $p->setValue(null, null); } diff --git a/tests/Integration/Seo/SchemaServiceExtraTest.php b/tests/Integration/Seo/SchemaServiceExtraTest.php index 238d02f3..256343dd 100644 --- a/tests/Integration/Seo/SchemaServiceExtraTest.php +++ b/tests/Integration/Seo/SchemaServiceExtraTest.php @@ -64,7 +64,6 @@ protected function tearDown(): void private function resetLatch(): void { $p = new ReflectionProperty(Seo_Service_Schema::class, '_emitted'); - $p->setAccessible(true); $p->setValue(null, false); } diff --git a/tests/Integration/Seo/SchemaServiceTest.php b/tests/Integration/Seo/SchemaServiceTest.php index d7acb9c0..2d7d78c8 100644 --- a/tests/Integration/Seo/SchemaServiceTest.php +++ b/tests/Integration/Seo/SchemaServiceTest.php @@ -65,7 +65,6 @@ protected function tearDown(): void private function resetLatch(): void { $p = new ReflectionProperty(Seo_Service_Schema::class, '_emitted'); - $p->setAccessible(true); $p->setValue(null, false); }